Atelier

Production management for an Australian commercial photography agency (Saunders & Co). Next.js 16 (App Router) + Supabase + TypeScript. Live at https://atelier.saundersandco.com.au.

Stack

Getting started

Requires Node 20+ and a Supabase project.

# 1. Clone + install
git clone <repo> && cd Atelier
npm install

# 2. Environment
cp .env.local.example .env.local   # if you have it; otherwise create .env.local manually
# Required at minimum:
#   NEXT_PUBLIC_SUPABASE_URL
#   NEXT_PUBLIC_SUPABASE_ANON_KEY
#   SUPABASE_SERVICE_ROLE_KEY
# Optional but recommended:
#   ANTHROPIC_API_KEY, GOOGLE_CLIENT_ID/SECRET/REFRESH_TOKEN
#   AGENCY_NAME, AGENCY_EMAIL, AGENCY_ABN, AGENCY_ADDRESS
#   CRON_SECRET (any strong random string)

# 3. Apply migrations
# Either via Supabase CLI:
supabase db push
# Or paste each file in supabase/migrations/ into the Supabase SQL editor in order.

# 4. Seed an owner so you can actually log in
# In Supabase SQL editor:
INSERT INTO atelier_app_users (email, role) VALUES ('you@example.com', 'owner');

# 5. Run
npm run dev

Open http://localhost:3000 → redirected to /login → enter the email you seeded → click the magic link in your inbox.

Scripts

Command What it does
npm run dev Next.js dev server
npm run build Production build
npm start Run the production build
npm run lint ESLint
npm test Vitest (~5s, 164 tests)
npm run test:watch Vitest in watch mode
npm run db:types Regenerate src/lib/types/database.generated.ts from the live schema
npx tsc --noEmit Strict TypeScript check

Architecture

src/
├── app/
│   ├── (dashboard)/         # owner + partner UI (gated by app_users role)
│   ├── portal/
│   │   ├── talent/          # talent self-service portal
│   │   └── crew/            # crew self-service portal
│   ├── onboard/             # /onboard (open) + /onboard/[token] (magic-link)
│   ├── q/[token]/           # public client quote viewer (token-gated, no auth)
│   ├── login/               # magic-link sign-in
│   ├── privacy/             # public privacy policy (APP-aligned)
│   ├── print/               # quote, invoice, confirmation, briefs, call sheet (light theme)
│   ├── api/                 # health, OAuth callbacks, cron, PDF endpoints, /api/onboard, etc.
│   └── actions/             # server actions — the only place mutations happen
├── components/              # client + server React, grouped by domain
├── lib/
│   ├── data/                # Supabase queries (one file per table)
│   ├── integrations/        # anthropic, xero, gmail, drive, calendar, google-auth
│   ├── automation/          # brief-intake, hold-requests, approval-effects, agent-primitives
│   └── utils/               # fee engine, brief parser, daterange, kill switch, audit, health…
├── middleware.ts            # session refresh + auth enforcement
supabase/
└── migrations/              # 47 SQL migrations, applied in order

Doctrine — non-obvious rules locked in code

Auth & roles

Integrations

Integration Status Required env
Anthropic Production (graceful no-op without key) ANTHROPIC_API_KEY
Google (Gmail · Drive · Calendar) Production GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN
Xero Stub — OAuth callback wired, invoice sync pending credentials XERO_CLIENT_ID, XERO_CLIENT_SECRET, XERO_REDIRECT_URI

One Google OAuth grant covers all three services (gmail.send/modify/readonly, drive.file, calendar.events). To set up: visit /api/auth/start/google to grant consent, paste the refresh token from the callback into env. Source: src/lib/integrations/{google-auth,gmail,drive,calendar}.ts.

drive.file (NOT full drive) is a deliberate scope choice — the app can only see files it created itself, which is exactly enough for booking folders without spooking Workspace admins or Google’s verification.

Cron jobs

Scheduled via vercel.json, gated by per-cron secrets (src/lib/utils/cron-auth.ts):

All comms crons queue approval-gated drafts; nothing sends without owner sign-off.

Database

47 migrations in supabase/migrations/, applied in order. Apply with supabase db push or by pasting each file into the Supabase SQL editor.

Tables (all prefixed atelier_): app_users, bookings, clients, brands, talent, crew, campaigns, booking_talent, booking_crew, booking_schedules, quote_versions, fee_lines, usage_licences, approvals, audit_log, llm_calls, idempotency_keys, kill_switch, locations, corpus_bookings, talent_preferred_crew, talent_unavailability, crew_unavailability, tasks, business_renewals, plus the atelier_bookings_portal view for column-restricted reads.

Regenerate types after schema changes:

npm run db:types

Then update src/lib/types/database.ts hand-types to match — both layers must move together (see CLAUDE.md “Schema change ripple”).

Testing

Run npm test. 164 tests / 11 files / ~5s. Coverage focuses on pure functions where silent regression is most expensive:

CI runs typecheck + lint + tests on every PR (.github/workflows/ci.yml). The check job must be green before merging.

Deployment

Vercel auto-deploys main to https://atelier.saundersandco.com.au. Migrations do NOT auto-apply — they’re a manual step via Supabase MCP or supabase db push. Set env vars in the Vercel dashboard; cron secrets are checked per-route by cron-auth.ts.

Where to start reading