Analysis · Current state

Analysis: current pics-portal scaffold analysis

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.

Stack

Framework
Next.js 16.2.6 (App Router, Server Components) on React 19.2.6, TypeScript 6, Node test runner via tsx --test.
UI
Tailwind 4.3, shadcn primitives (components/ui/*), Base UI 1.4 (render-prop pattern), lucide-react icons, sonner toasts, next-themes via custom provider.
Data
Zod 4 for schema parsing of Pics responses. Supabase JS + SSR clients (@supabase/ssr 0.10) for staged uploads only — no RLS in use, server actions use the service-role key.
Hosting
Local dev only so far. Next.js runs on port 3001 (pnpm dev).

Auth & sessions

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:

  1. Validating the portal_csrf cookie against the returned state.
  2. POSTing to /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.
  3. Writing three httpOnly Lax cookies: 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:

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.

Routes

RouteWhat it rendersPics 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]/uploadLegacy redirect to the event page, preserves returnTo + venueId.
/auth/callbackOAuth callback, sets cookies, redirects to /.POST /auth/exchange (HMAC-signed)
/auth/refreshClient-callable refresh endpoint.POST /auth/refresh
/auth/errorError page for failed auth.

Pics API client (lib/pics/client.ts)

Entities (all read-only):

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.

Supabase usage

One migration pair under supabase/migrations/:

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.

UI primitives

Test coverage

5 + 4 = 9 unit tests under tsx --test:

No integration or E2E suite yet.

Conspicuously missing for the larger feature set

  1. No subject model. Pics doesn't expose one; we don't store one.
  2. No export pipeline. Nothing exports anything — uploads are write-only, downloads don't exist.
  3. No layout / template / render system. Zero PDF generation, zero asset pipeline.
  4. No customer-facing portal. Every route is gated to Pics admins; no separate identity, no role model beyond super_admin, no invite flow.
  5. No async job substrate. Every server action is request-scoped.
  6. No image / S3 plumbing. Supabase Storage is in use for CSV staging only; no Pics image fetching, no signed URLs, no variants.
  7. No audit log beyond the uploads row trail.

Implications for the PRD