Submission platform for the Hackathon BH Onchain · Trilha Solana Superteam (Belo Horizonte, 13–17 May 2026). Hackers sign in, build a team, and submit their project. Edits stay open until the deadline; a cron locks every team after.
Co-organized by BH Onchain and curated by Solana Superteam BR. Inspired by the existing superteam-maker codebase.
npm install
cp .env.example .env.local # fill values
npm run dev # http://localhost:3000| Command | Purpose |
|---|---|
npm run dev |
Next.js dev server |
npm run build |
Production build (catches type errors) |
npm test |
Run vitest once |
npm run lint |
ESLint |
- Next.js 16 App Router + React 19 + TypeScript
- Supabase (Postgres, Auth, RLS, Storage)
- Tailwind CSS v4 (
@themetokens — notailwind.config) - Resend for transactional email (team invites + submission confirmation)
- Vitest for unit tests
- Vercel deploy + cron
| Var | Purpose |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
User-scoped client key |
SUPABASE_SERVICE_ROLE_KEY |
Server-only admin key (cron, invite issue) |
NEXT_PUBLIC_APP_URL |
Canonical app URL used in invite emails |
RESEND_API_KEY |
Resend API key (email skipped if unset) |
EMAIL_FROM |
"BH Onchain <hackathon@bhonchain.com>" |
INVITE_TOKEN_SECRET |
HMAC key for invite links (openssl rand -base64 48) |
CRON_SECRET |
Auth bearer for /api/cron/lock-submissions |
ADMIN_EMAIL_ALLOWLIST |
Comma-separated admin emails (curator panel + rating writes) |
ADMIN_USER_ID_ALLOWLIST |
Comma-separated admin user IDs |
Without RESEND_API_KEY, invite/confirmation emails are logged to the console (useful for local dev).
(public)/— landing, auth pages, legacy/invite/[token]magic-link landing.(app)/— auth-gated: onboarding, dashboard, team, submission editor,/admincurator panel (env-gated).api/— member remove, cron lock, signout, submit (legacy/api/team/inviteremoved in favor of the manual add-member server action).
Middleware (/middleware.ts) gates auth: any non-public route without a session redirects to /auth. Don't add auth checks in (app)/layout.tsx — that path causes redirect loops (we hit this in the parent project too).
auth.users ─trigger─> users (mirror + profile data)
└─> auto-links matching team_members ghost rows
hackathons (seeded with bh-onchain-2026)
teams ─trigger─> submissions + team_members(leader)
team_members (user_id may be null for ghost rows added by manual email lookup)
submission_ratings (one row per admin per submission)
Cross-table writes use two patterns:
SECURITY DEFINER RPCs for member-facing flows:
create_team_with_leader— validates the caller isn't already on a team for the hackathon, then inserts the team. Triggers auto-create the submission row and add the leader asaccepted.accept_team_invite— atomically attaches the authenticated user to a legacy token-based invite, with the same "one team per hackathon" guard.submit_team— leader-only finalize: validates required fields, flips submission tosubmitted, locks the team.auto_lock_overdue— runs from Vercel cron every 15 min to flip overdue draft submissions.
Server actions + service-role client for admin & team-leader writes that gate on env-allowlists:
addMemberByEmail(team leader) — manual member add. If the email is already registered the row goes in asacceptedimmediately; otherwise it sits as a ghost (user_id is null,status='pending') that the auth trigger links on signup.upsertRating/deleteRating(admin) — write tosubmission_ratings, which has RLS enabled with zero policies.
- Landing →
/auth→ Google OAuth or magic link. /auth/callbackexchanges the code and routes to:/onboardingifusers.full_nameorusers.luma_registered_atis missing./dashboardotherwise.
- Onboarding records
luma_registered_atafter the user checks the Luma confirmation box. The Luma link is surfaced prominently.
no team → /dashboard offers Criar time / Aguardar convite
↓ leader fills name
team (1 member) → leader adds up to 3 others by email:
· if the email is registered → instantly joined
· if not → ghost row, auto-links on signup
team (≤ 4 members) → all members can edit the submission
↓ leader clicks Submeter projeto (submit_team RPC, validates required fields)
team locked → submission frozen
Cron auto-locks any team past the deadline (whether submitted or not — draft becomes the final entry).
Legacy /invite/[token] magic-link flow is still wired up for any pre-existing token-based invites but is no longer the default; the UI uses manual email add.
Single page (/submission) with all fields. Two buttons:
- Salvar rascunho — persists every change with no required-field enforcement.
- Submeter projeto — leader-only. Confirms with the user, validates required fields, calls
submit_team.
Required deliverables match the regulamento:
- Repositório Git (private repos: the helper text instructs leaders to add
@kauenetas collaborator so judges have access) - Vídeo de apresentação (demo) — ≤ 3 min
- Deck — PDF/Notion/Slides with problema, solução, arquitetura, status, próximos passos
The cover image lives in the public Supabase Storage bucket project-images/{team_id}/{filename} (recommended 250 × 250 px, displayed as a square thumbnail in cards).
Env-gated by ADMIN_EMAIL_ALLOWLIST / ADMIN_USER_ID_ALLOWLIST. Three-column grid of submission cards; clicking opens a native <dialog> modal with the project's full detail, member socials, and a per-admin rating form (0–10 grade + comment). Each admin's rating is independent; the modal also lists every other admin's grade and comment for cross-referencing. Filter for Não avaliados / Avaliados and a "Por avaliar" stat help triage the judging queue.
We don't own the Luma event, so we can't query attendees directly. The user attests (with timestamp) that they're inscribed using the same email they used here. Organizers can later cross-reference the email column with a CSV export from the Luma organizer dashboard.
Hybrid BH Onchain (dark purple) + Superteam BR (yellow / emerald accents). See src/app/globals.css @theme block.
- Logos:
public/brand/bh/*(PNG) andpublic/brand/stbr/*(SVG). - Fonts: Outfit (heading) + Inter (body), loaded via
next/font/google.
- Create a new Supabase project.
- Apply the migrations in
supabase/migrations/in order via the SQL editor or the Supabase CLI. - Add
NEXT_PUBLIC_APP_URLto the project's allowed redirect URLs. - (Optional) Enable Google OAuth provider in Authentication → Providers.
Vercel — set the env vars, register the cron in vercel.json (already present):
/api/cron/lock-submissions — every 15 min
- UI copy in pt-BR. Code + routes in English.
.maybeSingle()over.single()when a row may not exist.(app)/pages declareexport const dynamic = 'force-dynamic'to avoid stale redirects.- Default to no code comments. If one is needed, explain WHY, not WHAT.
These are pragmatically out of scope for the first event:
- CSV export from the curator panel (the panel exists; only the export button is missing).
- Realtime presence in the submission editor (Supabase Realtime channel).
- Realtime updates of other admins' ratings as they're saved (today the panel revalidates on each save action).
- Direct Luma API integration once we own the event.
- Multi-hackathon support — schema already includes
hackathon_idon teams, but the UI hardcodesbh-onchain-2026.