Analysis · Current state
What the repo ships today (commit a1c25ee on claude/portal-scaffold-v1). Read-only browse over the Pics v1 API plus a staged-CSV upload to Supabase. Establishes the floor we're building up from.
tsx --test.components/ui/*), Base UI 1.4 (render-prop pattern), lucide-react icons, sonner toasts, next-themes via custom provider.@supabase/ssr 0.10) for staged uploads only — no RLS in use, server actions use the service-role key.pnpm dev).The Pics admin login flow lives in app/auth/callback/route.ts. We mint a CSRF state cookie, redirect to Pics for login, then handle the callback by:
portal_csrf cookie against the returned state./api/portal/v1/auth/exchange with the auth code, signed by SHA-256 HMAC of the raw body (header x-portal-signature, secret in PICS_PORTAL_SIGNING_SECRET). Implementation: lib/pics/signing.ts, lib/pics/auth.ts.portal_session (Bearer token, JWT-like), portal_admin_id (extracted admin id, fallback parsed from token claims admin_id|adminId|sub|id), and clearing portal_csrf.Session keepalive is layered:
proxy.ts) — Next.js middleware matcher; if the token's exp claim is within ~120s, it preemptively refreshes via Pics /api/portal/v1/auth/refresh and rewrites the cookies on the response.components/session-keepalive.tsx) — POST /auth/refresh every 4 minutes plus on visibilitychange. Threshold ≤ 5 min remaining; ?force=1 bypasses the threshold. Returns { refreshed, reason? }.RBAC today is a single boolean. The Pics session returns company_user.super_admin? and a list of companies. Nothing else. There is no concept of a non-Pics user, customer, or role.
| Route | What it renders | Pics calls |
|---|---|---|
/ | Studio picker (redirects to /companies/[id] if only one). | listCompanies() |
/companies/[companyId] | Venues table with search + pagination, studio switcher header. | listCompanies(), listVenues(companyId, {page, perPage, query}) |
/venues/[venueId] | Events table with pagination. | listEvents(venueId, {page, perPage}) |
/events/[eventId] | Event detail card with the CSV staging form. | findEventForVenue(venueId, eventId) (paginated linear search) |
/events/[eventId]/upload | Legacy redirect to the event page, preserves returnTo + venueId. | — |
/auth/callback | OAuth callback, sets cookies, redirects to /. | POST /auth/exchange (HMAC-signed) |
/auth/refresh | Client-callable refresh endpoint. | POST /auth/refresh |
/auth/error | Error page for failed auth. | — |
lib/pics/client.ts)Entities (all read-only):
Company — { id, name, venue_count? }Venue — { id, company_id|companyId, name, city?, state?, event_count? }PicsEvent — { id, venue_id|venueId, name?, shoot_date?, starts_on|startsOn?, date?, season?, year?, event_type?, key? } with a Zod transform that falls back to "Event {id}" when name is blank.Schemas accept both { data: [...], pagination: {...} } envelopes and bare arrays. year is permissive (string | number) because Pics is inconsistent.
Pagination is offset-based: { current_page, total_pages, total_count, per_page }. page is 1-based, per_page defaults to 50, clamped to 100.
Helper picsFetch<T>(path, schema, init?) adds the Bearer header, parses through the Zod schema, and triggers a sign-out redirect on 401 expired_token.
One migration pair under supabase/migrations/:
uploads table: { id uuid, event_id text, uploaded_by_admin_id text, storage_path text unique, status enum('staged'), uploaded_at, created_at } with indexes on event_id and uploaded_at desc.uploads bucket (private, 50 MB, MIME-restricted to CSV variants).The stageCsvUpload server action in app/events/[eventId]/actions.ts validates the file, uploads to {eventId}/{uuid}.csv, and inserts a row with status='staged'. There is no read path back out — uploads are write-only in v1, and the contract explicitly notes Pics push is disabled.
Badge, Button (render-prop), Card, Input, Label, Spinner, Table, custom DropdownMenu, Sonner toast wrapper.PortalShell (header + sticky nav + SessionKeepalive), PageHeader, StudioSwitcher (select-based), VenuesTable (search + pagination), PaginationControls, ThemeToggle, ThemeProvider.5 + 4 = 9 unit tests under tsx --test:
lib/format.test.ts — date formatting timezone-safe.lib/pics/client.test.ts — envelope parsing and the venues path builder.lib/pics/session.test.ts — refresh thresholds (both readable and opaque tokens).components/venues-table.test.tsx — search URL behavior, pagination reset, count copy, debounce timing.No integration or E2E suite yet.
super_admin, no invite flow.uploads row trail.lib/pics/client.ts is the template every new Pics-endpoint integration should follow.