QR-first campus place archive for the DevSoc Flagship Hackathon theme: connection through places, time, and students who never meet.
The QR code is a physical object outside the app.
Intended demo flow:
physical QR code
-> phone camera
-> deployed /desk/47
-> Desk 47 archive page
The app is the destination after scanning. It does not generate QR codes, scan QR codes, or use browser camera APIs.
Hour 0-2 is complete locally:
- Next.js App Router project is set up.
- TypeScript, Tailwind, ESLint, Supabase, and OpenAI dependencies are installed.
- Supabase env vars are configured locally.
/api/healthcan fetch Desk 47 from Supabase.- Minimum tables exist:
locations,profiles, andmessages.
Hour 2-7 is complete locally:
/desk/47renders the Desk 47 archive destination page.- The page fetches public messages from Supabase.
- Desk 47 has seeded demo messages.
- The page shows mobile-friendly message cards with anonymous author labels, timestamps, tags, course tags, and upvotes.
- The page has a message form.
POST /api/messagesvalidates and saves new public messages.- New posts get a stable per-desk anonymous label such as
Desk-47 Lantern.
Hour 7-12 is complete locally:
- A fresh browser sees onboarding before the Desk 47 archive.
- Demo personas are available inside onboarding: Alex and Jamie.
- Manual course selection is available with the static course list.
- The selected study profile is saved in browser
localStorageundermystudyfriend_profile. - Returning in the same browser skips onboarding.
- The archive shows a profile banner and a
Switch profilecontrol. - Supabase Auth is intentionally not built yet.
Hour 12-18 Stage A is complete locally:
- Desk 47 messages are reordered from the saved local study profile.
- Alex sees COMP2521/MATH1081 messages near the top.
- Jamie sees FINS1613/ECON1101 messages near the top.
- The top 3 ranked messages show small "why this message" labels.
- Ranking is deterministic and does not require OpenAI.
Hour 12-18 Stage B fallback layer is implemented:
POST /api/ranked-messagesreturns ranked messages for the local profile.- If embeddings are unavailable, it returns Stage A deterministic ranking.
- If
OPENAI_API_KEY, pgvector, and embedded rows are available, it adds semantic similarity. POST /api/admin/embed-messagescan embed missing public messages in small batches.- The admin embedding route requires
ADMIN_SECRETin production andSUPABASE_SERVICE_ROLE_KEYfor database updates.
Still not complete:
- Vercel deployment has not been verified from this machine.
- Phone-camera QR scan has not been verified against a deployed URL.
APP_SECRETshould be configured before production/demo deployment.OPENAI_API_KEYmust be configured before posting new public messages.SUPABASE_SERVICE_ROLE_KEYandADMIN_SECRETare needed only for Stage B embedding administration.
npm install
npm run devOpen:
http://localhost:3000http://localhost:3000/desk/47http://localhost:3000/api/health
Copy .env.example to .env.local and fill in:
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
OPENAI_API_KEY=
APP_SECRET=
SUPABASE_SERVICE_ROLE_KEY=
ADMIN_SECRET=Needed now:
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYAPP_SECRETOPENAI_API_KEY
APP_SECRET is server-only. Do not name it NEXT_PUBLIC_APP_SECRET. It is used to create stable anonymous per-desk labels from the browser's local demo user id.
OPENAI_API_KEY is server-only. It is used by POST /api/messages to run OpenAI Moderation before saving a new public message.
Needed later:
SUPABASE_SERVICE_ROLE_KEYfor the admin embedding batch routeADMIN_SECRETto protect admin routes in production
Local env note:
- When running the app from the
DeskSupportfolder, Next.js readsDeskSupport/.env.local. - A
.env.localin the parentHackathonfolder will not activate Stage B for this app. - Never commit
.env.local.
Run these SQL files in the Supabase SQL editor:
supabase/schema.sqlsupabase/seed.sql
The schema includes messages.author_label, used as the canonical anonymous per-desk display label.
The schema also includes the optional Stage B vector setup:
messages.embedding extensions.vector(1536)match_messages_for_location(...)
If pgvector setup blocks demo prep, leave embeddings empty. The app falls back to deterministic Stage A ranking.
To verify the optional Stage B schema, run:
select extname from pg_extension where extname = 'vector';
select column_name, data_type, udt_name
from information_schema.columns
where table_schema = 'public'
and table_name = 'messages'
and column_name = 'embedding';
select routine_name
from information_schema.routines
where routine_schema = 'public'
and routine_name = 'match_messages_for_location';If your Supabase project existed before this cleanup, rerun supabase/schema.sql so the existing messages table gets the new author_label column.
If old rows already have good values in pseudonym but still show author_label = 'Anonymous Student', run this backfill in the Supabase SQL editor:
update messages
set author_label = pseudonym
where author_label = 'Anonymous Student'
and pseudonym is not null
and pseudonym <> ''
and pseudonym <> 'Anonymous Student';This copies existing good pseudonyms into the new canonical author_label column. Rows where both fields are Anonymous Student are old test data and can be cleaned manually before the final demo.
The seed file includes:
- 8 locations
- 28 Desk 47 public messages
- 4 messages across other demo locations
Stage B is optional. The demo still works with Stage A if any embedding setup is missing.
- Add these to
DeskSupport/.env.local:
OPENAI_API_KEY=
SUPABASE_SERVICE_ROLE_KEY=
ADMIN_SECRET=- Restart the dev server:
Ctrl+C
npm run dev-
Run the latest
supabase/schema.sqlin the Supabase SQL editor. -
Check what the local Next.js app can see:
Invoke-RestMethod http://localhost:3000/api/debug/env-checkExpected shape:
{
"hasOpenAIKey": true,
"hasServiceRoleKey": true,
"hasAdminSecret": true
}This endpoint only exists in development and returns booleans, never secret values.
- Check the admin embedding status:
Invoke-RestMethod `
-Method Get `
-Uri http://localhost:3000/api/admin/embed-messages `
-Headers @{ "x-admin-secret" = "<ADMIN_SECRET_FROM_ENV_LOCAL>" }The response should include:
totalPublicMessages
missingEmbeddings
embeddedMessages
- Embed a small batch:
Invoke-RestMethod `
-Method Post `
-Uri http://localhost:3000/api/admin/embed-messages `
-ContentType "application/json" `
-Headers @{ "x-admin-secret" = "<ADMIN_SECRET_FROM_ENV_LOCAL>" } `
-Body '{"batchSize":10}'Repeat until missingEmbeddings is 0.
- Test ranked messages:
Invoke-RestMethod `
-Method Post `
-Uri http://localhost:3000/api/ranked-messages `
-ContentType "application/json" `
-Body '{"locationId":47,"profile":{"profileId":"alex","displayName":"Alex","courses":["COMP2521","MATH1081"]}}'Expected behavior:
- If env vars exist but no messages are embedded yet,
modestaysstage-aand the warning says no embedded messages were found. - If pgvector or the RPC is missing,
modestaysstage-aand the warning says vector search is not ready. - If OpenAI rejects the request because of quota, billing, or key permissions,
modestaysstage-aand the warning says the OpenAI fallback reason. - After embeddings exist,
modebecomesstage-band messages includesemantic_similarity.
npm run lint
npm run buildThen check:
/api/healthreportsdesk47.source: "supabase"./api/healthreports a positivedesk47Messages.count.- Clearing
localStorageand opening/desk/47shows onboarding. - Selecting Alex unlocks the archive and shows
Personalised for Alex · COMP2521 · MATH1081. - Refreshing
/desk/47skips onboarding in the same browser. - Switching to Jamie updates the banner to
Personalised for Jamie · FINS1613 · ECON1101. - Alex's top messages include COMP2521/MATH1081 content and why labels.
- Jamie's top messages include FINS1613/ECON1101 content and why labels.
/desk/47does not show a QR panel or raw localhost/deployed URL.- Submitting a safe message returns HTTP 201 from
POST /api/messages. - Submitting a URL, email, phone number, obvious abuse, or OpenAI-flagged unsafe message is blocked before insert.
- Refreshing
/desk/47shows the submitted message. - A new message has a stable anonymous label and automatic demo tags.
- Supabase has the
messages.author_labelcolumn after rerunningsupabase/schema.sql.
app/desk/[locationId]/page.tsxrenders the desk archive destination page.app/desk/[locationId]/desk-archive-client.tsxchecks the local study profile, shows onboarding, and unlocks the archive.app/desk/[locationId]/message-composer.tsxhandles the browser-side message form and local demo user id.app/api/ranked-messages/route.tsranks messages from the local profile, using embeddings only when available.app/api/admin/embed-messages/route.tsembeds missing public messages for developers.app/api/messages/route.tssaves new messages to Supabase and creates author labels.app/api/health/route.tsreports location/message health.lib/author-label.tschooses the best display label during thepseudonymtoauthor_labelmigration.lib/moderation.tsblocks contact info/obvious abuse, calls OpenAI Moderation, and creates demo tags.lib/pseudonym.tscreates stable anonymous per-desk labels.lib/ranking.tsbuilds the profile context string and ranks messages for the saved study profile.lib/openai.tscreates embeddings withtext-embedding-3-small.lib/vector.tsconverts embedding arrays into pgvector literals.lib/locations.tsfetches locations with fallback data.lib/messages.tsfetches messages with fallback data.lib/demoData.tsstores local fallback locations and messages.supabase/schema.sqlcreates tables and RLS policies.supabase/seed.sqlseeds demo locations and messages.
docs/hour-0-2-debugging-handoff.mddocs/hour-2-7-debugging-handoff.mddocs/hour-7-12-debugging-handoff.mddocs/hour-12-18-debugging-handoff.mddocs/detailed-execution-guide.md
Read the relevant handoff before continuing to the next checkpoint.