Member and event tracking for the V1 Michigan community team. Implements
v1-gtm-dashboard-architecture.md §1–10; §11 is deliberately not built.
cp .env.example .env # fill in SEED_ADMIN_EMAIL at minimum
pnpm install
supabase start # Postgres + Auth + Storage + Studio, migrations and seed applied
make dev # or: pnpm devmake reset re-applies every migration and reseeds. make tunnel prints a
public URL for the Slack and Tally webhook routes during development.
DEV_BYPASS_AUTH=true injects a fixed admin session and runs server reads
through the service-role key, bypassing RLS. It is honoured in every
environment, production included — while it is on, anyone who can reach the
site has full admin access without signing in. See lib/flags.ts.
See performance notes for the load-time changes, local benchmark results, and commands to verify the RLS optimization.
app/(auth)/login email + password sign-in, and the rejected-account notice
app/(dashboard) every admin page; middleware 404s the `member` role here
app/api imports, export, slack events, webhook stubs
lib/supabase server (RLS) / browser / service-role clients
lib/matching normalize + matchPerson, the TS mirror of the SQL ladder
lib/imports per-kind Zod schemas, the §4.9 column mappings, parsing
lib/queries.ts the cached read layer — every page renders from here
lib/cache.ts tag constants + withCache/revalidate
supabase/migrations the only way the schema changes
netlify/functions slack-sync, daily
fixtures/csv one sample file per import kind
- Reads go through
lib/queries.ts. Each is wrapped inwithCache(keyParts, tags, fn); writes callrevalidate(...)with the tags they touched. Counts come from SQL views, never from JS. - Types live in
lib/types.tsand are attached at the query boundary. The file mirrorssupabase/migrations; regenerate withsupabase gen typesif it drifts. - Server Actions do mutations. Any write to a
peopleoreventsfield also writes afield_changesrow — call the SQL helpers rather than duplicating that. - The service-role client is only legal inside
app/api/webhooks/*,app/api/slack/*, andnetlify/functions/*, each of which verifies its own signature.
The V1 website visual language is consolidated in app/v1-theme.css: cream
backgrounds, charcoal text, gold accents, Instrument Serif display headings,
and Inter interface text. app/globals.css exposes the same semantic tokens
to Tailwind v4. See the design system for source
references, color roles, component guidance, and accessibility adaptations.
Deviation from spec §5, recorded here as the spec asks. The spec fixes Google as the only provider with email/password disabled. This build uses email
- password instead, at the owner's direction, with no self-serve sign-up —
an admin provisions each account in
/settingsand hands over a one-time password. Creating an auth user needs the Admin API, soprovisionUser()is the one Server Action that touches the service-role client; it is guarded byrequireAdmin()and writes nothing butapp_usersandauth.users. The access rule is unchanged and is still enforced where it always was: theauth.userstrigger in the baseline migration refuses any address that is not@umich.eduand either onapp_usersor attached to a person withis_v1_member = true. Switching provider does not widen who can get in. Email confirmations are off, so no SMTP is required.
One deviation from spec §2, recorded here as the spec asks. The spec names
shadcn/ui. The dashboard already ships .btn, .card, .table, .tag, .input,
.seg and .dialog as finished CSS, so generating shadcn's components and then
re-theming each one against those same variables would be strictly more code for
identical pixels. Instead components/ui/ wraps Radix primitives — the parts
that carry real behaviour and accessibility (dialog, popover, combobox, radio
group, checkbox) — in the shared design-system classes, and everything purely visual uses the
classes directly. TanStack Table, Tailwind and the token mapping are unchanged.