1 Overview ▶
Summit Editing is a server-rendered React app on TanStack Start + Vite 7, deployed to Cloudflare Workers. The backend is Supabase Postgres, with all authorization enforced in the database via Row-Level Security (RLS) and SECURITY DEFINER functions keyed off the Supabase-issued JWT (auth.uid()). Payments run through Stripe (embedded checkout, hand-rolled webhook verification), and transactional email is a Supabase-native pgmq queue + pg_cron drainer pipeline rendered with React Email. A thin Lovable Cloud layer supplies OAuth, email transport, webhook verification, and the Vite plugin chain — only ~4 application files import @lovable.dev/*.
Request lifecycle
┌─────────────────────────────────────────────────────────┐
Browser │ Cloudflare Worker (entry: src/server.ts) │
─────── │ ───────────────────────────────────────── │
React SPA / SSR ────▶ │ server.ts ── catches catastrophic SSR errors, │
fetch + serverFn │ │ renders branded 500 page │
(Bearer JWT) │ ▼ │
│ @tanstack/react-start/server-entry │
│ ├─▶ File routes (src/routes/**) │
│ │ • React pages (SSR + hydrate) │
│ │ • Server routes (.ts API handlers) │
│ └─▶ Server functions (createServerFn) │
│ start.ts middleware: │
│ • errorMiddleware (request) │
│ • attachSupabaseAuth (function, client side) │
└─────────────────────┬────────────────────────────────────┘
│ Authorization: Bearer <supabase JWT>
▼
┌───────────────────────────┐
│ Supabase Postgres │
│ • RLS policies (auth.uid)│
│ • SECURITY DEFINER RPCs │
│ • has_role() gate │
│ • pgmq + pg_cron (email) │
└───────────────────────────┘
src/server.ts— thin wrapper around@tanstack/react-start/server-entry; normalizes h3's swallowed{"unhandled":true,"message":"HTTPError"}500s into a branded error page (normalizeCatastrophicSsrResponse).src/start.ts— registers a request-level error boundary and the function-levelattachSupabaseAuthmiddleware that attaches the user's Supabase access token as a Bearer header on every server-function RPC.- The browser holds the Supabase session; every gated read/write carries that JWT into Postgres, where RLS and
has_role()decide what's permitted.
2 Directory map ▶
Summit Edits Pro/ ├── src/ │ ├── server.ts Cloudflare Worker entry — SSR error normalizer + branded 500 │ ├── start.ts createStart(): request error mw + attachSupabaseAuth fn mw │ ├── router.tsx TanStack Router instance (QueryClient wiring) │ ├── routeTree.gen.ts GENERATED route tree — never hand-edit │ ├── styles.css Tailwind v4 entry, OKLCH theme tokens, --neon brand color │ │ │ ├── routes/ File-based routes (pages + server route handlers) │ │ ├── __root.tsx Root layout / document shell │ │ ├── _authenticated.tsx Auth-gated layout: redirects to /login when no user │ │ ├── _authenticated/ Role-split gated pages (dashboard.* / editor.* / admin.*) │ │ ├── api/public/* Server routes: contact, payments/webhook, hooks/editor-due-soon │ │ ├── lovable/email/* Lovable email infra routes (auth webhook, queue drain, suppression) │ │ ├── email/unsubscribe.ts One-click unsubscribe handler │ │ ├── sitemap[.]xml.ts Sitemap │ │ └── (flat public pages) index, pricing, faq, how-it-works, submit, buy-credits, │ │ checkout.return, login, signup, forgot/reset-password, contact │ │ │ ├── integrations/ │ │ ├── lovable/index.ts [GENERATED] Lovable OAuth → sets Supabase session │ │ └── supabase/ │ │ ├── client.ts [GENERATED] Lazy Proxy browser client (anon key) │ │ ├── client.server.ts Server-side admin (service-role) client │ │ ├── auth-middleware.ts [GENERATED] requireSupabaseAuth (server fn guard, getClaims) │ │ ├── auth-attacher.ts [GENERATED] attachSupabaseAuth (client fn mw, Bearer) │ │ └── types.ts [GENERATED] Full Database type from schema │ │ │ ├── lib/ │ │ ├── orders.functions.ts Order lifecycle server fns (assign, status, revision, cancel…) │ │ ├── inbox.functions.ts Admin contact-inbox server fns │ │ ├── chat.functions.ts In-app chat/notes server fns │ │ ├── due-soon-emails.functions.ts Admin due-soon email tooling │ │ ├── stripe.ts / stripe.server.ts Stripe client + Workers-safe webhook verify │ │ ├── gmail.server.ts Inbound contact email integration │ │ ├── error-capture.ts / error-page.ts SSR error capture + branded HTML │ │ ├── email/ enqueue.server.ts (pgmq enqueue), send.ts │ │ ├── email-templates/ React Email templates + registry.ts │ │ └── utils.ts cn() helper │ │ │ ├── utils/payments.functions.ts Stripe checkout-session server fns │ ├── hooks/ use-auth (client user), use-mobile, use-video-observer │ ├── components/ App components + components/ui (shadcn New York) │ └── assets/ Static imports │ ├── supabase/ │ ├── config.toml │ └── migrations/ ~39 SQL migrations (schema, RLS, RPCs, email infra, │ package_prices, editor-availability RPC, invite gate) │ ├── public/ Marketing video/asset payload (banner, ai-samples, samples) ├── .lovable/ project.json, plan.md (editor-workflow scope decisions) ├── vite.config.ts Wraps @lovable.dev/vite-tanstack-config (do NOT add plugins) ├── wrangler.jsonc Cloudflare Workers config (main: src/server.ts, nodejs_compat) ├── bun.lock / bunfig.toml Bun is the package manager of record └── package-lock.json Leftover Lovable artifact — ignore
Generated files (
routeTree.gen.ts, theintegrations/**Lovable/Supabase files,types.ts) carry a "do not modify" header. Regenerate rather than hand-edit.
3 Routing model ▶
File-based routing in src/routes/ via @tanstack/router-plugin. Three shapes of route.
Public marketing / auth pages flat in src/routes
index, pricing, faq, how-it-works, submit, buy-credits, checkout.return, login, signup, forgot-password, reset-password, contact, ai-examples, unsubscribe. No auth required.
_authenticated.tsx — the auth gate
A layout route that reads useAuth(); if there is no user after loading, it navigate({ to: "/login", search: { redirect } }). Everything under src/routes/_authenticated/ sits behind it. This is a client-side UX guard — real authorization is enforced server-side by RLS and the requireSupabaseAuth server-fn middleware.
Gated pages are role-prefixed (roles live in the user_roles table, not on the route):
| Prefix | Role | Pages |
|---|---|---|
dashboard.* | customer | index (order list), orders.$id (detail), orders.$id.delivery, profile |
editor.* | editor | index (queue), orders.$id (workspace), availability (active toggle + max concurrent) |
admin.* | admin | index, editors (workload + assign UI), inbox (contact), orders.$id (detail), due-soon-emails |
Server routes .ts handlers, not React pages
| Path | Purpose |
|---|---|
api/public/contact.ts | Public contact-form submission |
api/public/payments/webhook.ts | Stripe webhook (signature-verified, ?env=sandbox|live) |
api/public/hooks/editor-due-soon.ts | pg_cron-driven editor "due soon" alerts (Bearer CRON_HOOK_SECRET) |
lovable/email/auth/webhook.ts | Supabase Auth email webhook → enqueue auth emails |
lovable/email/auth/preview.ts | Auth template preview (dev) |
lovable/email/queue/process.ts | Queue drainer — pgmq read → send → log/DLQ (Bearer service-role) |
lovable/email/suppression.ts | Bounce/complaint webhook → suppression list |
lovable/email/transactional/send.ts · preview.ts | Transactional send + preview |
email/unsubscribe.ts | One-click unsubscribe (token) |
sitemap[.]xml.ts | Sitemap |
Auth middleware (two halves)
- Client side —
attachSupabaseAuth(auth-attacher.ts), a globalfunctionMiddlewareinstart.ts: reads the browser session and attachesAuthorization: Bearer <access_token>to every server-function call. - Server side —
requireSupabaseAuth(auth-middleware.ts): validates the Bearer token viasupabase.auth.getClaims(token), derivesuserId = claims.sub, and yields a request-scoped Supabase client (bound to the user's JWT) into the server-fn context — so RLS applies to everything that client does.
4 Data model ▶
Postgres via Supabase. ~39 migrations in supabase/migrations/. Authoritative generated schema: src/integrations/supabase/types.ts.
Enums
app_role:admin|customer|editor— the DB value iscustomereven though UX copy says "client";editorwas added after the initial migration.payment_status:unpaid|paid|refunded.order_status— the lifecycle:
Core tables
| Table | Holds |
|---|---|
profiles | User profile + credit_balance, editor capacity (is_editor_active, max_concurrent_orders default 5, editor_specialty, editor_bio) |
user_roles | Roles as rows (not a profile column); checked via has_role(uid, role) |
orders | The job. user_id, package_name/package_slug, amount_cents/currency (default CAD), footage_link, special_instructions, status, payment_status, stripe_session_id/stripe_payment_intent, assigned_to, due_at, delivered_at, credits_spent, clip_count/overage_credits, revision_count, due_soon_alerted_at, admin_notes |
order_deliveries, order_delivery_downloads | Delivered files + download tracking |
order_revision_attachments | Files attached to revision requests |
order_status_history | Audit trail of every status change |
order_internal_notes | Staff-only internal notes |
notifications | In-app notifications |
credit_transactions | Ledger of credit deltas (purchases, edit spends, refunds) |
contact_submissions | Public contact form |
package_prices, addon_prices | Server-side authoritative price tables (slug → credits) |
invite_codes | Invite-gate codes (no client read access; validated server-side) |
email_send_log, email_send_state, email_unsubscribe_tokens, suppressed_emails | Email infra: idempotency, rate-limit cooldown, unsubscribe, suppression |
Where authorization lives
- RLS everywhere. Customers
selectonly their own orders and mayupdatethem only whileawaiting_footage; aBEFORE UPDATEtrigger (orders_restrict_customer_updates) hard-limits non-admins to changing onlyfootage_link/special_instructions. Admins have full access; editors are scoped toassigned_to = auth.uid(). has_role(uid, role)—SECURITY DEFINERfunction overuser_roles; the single source of truth for role checks across policies and RPCs.prevent_privileged_profile_updatestrigger blocks editors/customers from changingcredit_balance,is_editor_active,max_concurrent_ordersdirectly; the editor-availability RPC bypasses it via a transaction-scoped GUC signal.
Key SECURITY DEFINER RPCs
| RPC | What it does |
|---|---|
submit_edit_order(...) | Customer creates an order. Costs it from the server-side package_prices/addon_prices tables (validates slug, raises on unknown), applies per-clip overage, debits credit_balance under FOR UPDATE, sets due_at (24h rush / 48h standard), inserts the order + a credit_transactions row. |
assign_order(_order_id, _editor_id) | Admin-only. Validates target is an active editor, soft-warns on over-capacity (still assigns), updates orders.assigned_to, inserts in-app notification for the new editor (and removal notice for the previous one). Returns JSON incl. over_capacity; the editor-assignment email is enqueued from the server fn. No auto-assignment. |
set_editor_availability(_is_active, _max_concurrent) | Editor-only. Updates own availability fields. Sets a transaction-scoped GUC (app.bypass_profile_guard='editor_availability') so the profile-guard trigger permits exactly these two fields, then resets it. |
request_revision(_order_id, _note, _attachments) | Customer requests a revision on a delivered order; records note + attachments and moves the order back into the revision flow. |
set_revision_stage(_order_id, _stage) | Moves an order's status backward/forward through revision sub-stages. |
cancel_order(...) | Cancels an order (status → cancelled) with refund/credit handling. |
editor_workload_summary() | Per-editor active/overdue/awaiting/in-progress/delivered-30d counts + capacity (staff dashboard). |
validate_invite_code(_code) | Called inside handle_new_user; locks + decrements an invite_codes row, raising if missing/invalid/expired/exhausted. Globally disable via app.invite_gate='off'. |
handle_new_user() | AFTER INSERT trigger on auth.users: enforces the invite gate, creates the profiles row, grants default customer role. Fires for all signup paths. |
enqueue_email, read_email_batch, move_to_dlq, delete_email | pgmq wrappers for the email pipeline. |
5 Data-flow diagrams ▶
(a) Auth / session flow
Signup: signup.tsx → supabase.auth.signUp({ ..., data: { invite_code } })
→ handle_new_user() trigger: validate_invite_code() (gate)
→ create profiles row + grant 'customer' role
Gate (client): _authenticated.tsx → no user? → navigate /login
(b) Order lifecycle
Customer (submit.tsx) ── submit_edit_order(slug, addons, footage…)
→ costs from price tables, debits credits, sets due_at
│
▼ awaiting_footage (customer may still edit footage_link / instructions)
│ admin: assign_order(order, editor) → notification + editor-assignment email
▼ accepted → uploading_footage → in_progress (editor works the order)
▼ ready_for_delivery
│ editor uploads files → notifyDeliveryUploaded (delivery-file-added email)
▼ delivered
│ customer: request_revision(note, attachments)
▼ set_revision_stage moves status back → in_progress (revision loop)
Any state ──▶ cancelled (cancel_order; refund/credit handling)
Every status change → order_status_history (audit) + order-status-update email
(c) Email queue pipeline Supabase-native
Producers
─────────
• Server fns (order events) ── enqueueTemplateEmail() ─┐ lib/email/enqueue.server.ts
• editor-due-soon hook ───────────────────────────────┤ - idempotency: skip if message_id
• Supabase Auth webhook (auth emails) ────────────────┘ already in email_send_log
• suppression check: skip + log if recipient suppressed
│ rpc enqueue_email(queue, payload{ html, subject, ... })
▼
┌──────────────────────────────────────────────┐
│ pgmq queues │
│ • auth_emails (TTL ~15 min) │
│ • transactional_emails (TTL ~60 min) │
└───────────────────┬──────────────────────────┘
│ pg_cron periodically POSTs (Bearer = service-role key)
▼
/lovable/email/queue/process (queue drainer)
1. check email_send_state.retry_after_until (rate-limit cooldown)
2. read_email_batch (auth first, then transactional; vt=30)
3. per message:
• drop if past TTL → move_to_dlq
• skip if already 'sent' (VT-race guard) → delete_email
• sendLovableEmail() (@lovable.dev/email-js)
├─ success → log 'sent' + delete_email
├─ 429 → log 'failed', set retry_after_until, STOP
├─ 403 → move_to_dlq, STOP
└─ other → log 'failed' (retries until MAX_RETRIES → DLQ)
▼
Provider sends → bounce/complaint webhook
▼
/lovable/email/suppression → suppressed_emails (future sends skipped)
React Email templates live in src/lib/email-templates/*.tsx, registered in registry.ts (TEMPLATES[name] = { component, subject }) and rendered to static HTML at enqueue time.
(d) Stripe credit-purchase flow
buy-credits.tsx
│ payments.functions.ts → create Stripe Checkout Session
▼ (metadata.userId, price lookup_key e.g. credits_500_usd)
StripeEmbeddedCheckout.tsx (embedded UI, gateway via connector-gateway.lovable.dev)
│ customer pays
▼
Stripe ── checkout.session.completed ──▶ /api/public/payments/webhook?env=…
│
▼
verifyWebhook() (lib/stripe.server.ts)
• parse stripe-signature (t=, v1=)
• reject if timestamp age > 300s
• HMAC-SHA256 over `${t}.${body}` via WebCrypto
(Workers-safe; no Stripe SDK constructEvent)
│
▼
handleCheckoutCompleted (service-role client)
• idempotency: skip if session already in credit_transactions
• re-fetch session, expand line_items.price
• map lookup_key → credits (CREDITS_BY_PRICE)
• INSERT credit_transactions(delta = credits)
• UPDATE profiles.credit_balance += credits
✗ does NOT create an orders row (credit buys ≠ jobs)
│
▼
checkout.return.tsx shows confirmation; credits now spendable via submit_edit_order
6 Integrations ▶
| Integration | Role | Key files |
|---|---|---|
| Supabase | Postgres DB, Auth (JWT), RLS, RPCs, pgmq/pg_cron email engine | src/integrations/supabase/*; supabase/migrations/** |
Lovable Cloud — cloud-auth-js | OAuth only (email/password is native Supabase) | src/integrations/lovable/index.ts |
Lovable Cloud — email-js | Transactional send + auth-payload parse | src/routes/lovable/email/queue/process.ts, src/routes/lovable/email/auth/webhook.ts |
Lovable Cloud — webhooks-js | Webhook signature verification | src/routes/lovable/email/suppression.ts, src/routes/lovable/email/auth/webhook.ts |
Lovable Cloud — vite-tanstack-config | Vite plugin chain | vite.config.ts |
| Stripe | Credit payments, embedded checkout, Workers-safe webhook verify | src/utils/payments.functions.ts, src/lib/stripe.ts, src/lib/stripe.server.ts, src/components/StripeEmbeddedCheckout.tsx, src/routes/api/public/payments/webhook.ts |
| Transactional + auth email via pgmq/pg_cron + React Email | src/lib/email/enqueue.server.ts, send.ts, src/lib/email-templates/*, src/routes/lovable/email/**, src/routes/email/unsubscribe.ts | |
| Gmail (inbound) | Contact-email integration | src/lib/gmail.server.ts |
Email/password auth is native Supabase (
signInWithPassword/signUp). Only OAuth goes through Lovable. Email transport is Supabase-native (pgmq); Lovable only supplies the finalsendLovableEmailtransport call. Exactly 4 packages couple the app to Lovable, across the files above.
7 Deployment & environment ▶
Platform
- Cloudflare Workers via Wrangler —
wrangler deploy. wrangler.jsonc:main: src/server.ts,compatibility_flags: ["nodejs_compat"],compatibility_date: 2025-09-24, app nametanstack-start-app.- Deploys also flow through Lovable Cloud, which provisions Supabase/auth/email env.
Build / tooling
- Bun is the package manager of record (
bun.lock+bunfig.toml).package-lock.jsonis a leftover Lovable artifact. The bunfig supply-chain guard skips packages published < 24h ago. vite.config.tswraps@lovable.dev/vite-tanstack-config, which already injectstanstackStart,viteReact,tailwindcss,tsConfigPaths, the Cloudflare plugin,VITE_*env, the@alias, dedupe, and error-logger plugins. Do not add those plugins manually. Theentitiesalias workaround is load-bearing for react-markdown.- No standalone typecheck script;
tscruns withnoEmit.
Commands
bun install # respects 24h supply-chain guard bun run dev # Vite dev server (TanStack Start) bun run build # production build (build:dev for development mode) bun run preview # preview a production build bun run lint # eslint . bun run format # prettier --write . wrangler deploy # deploy to Cloudflare Workers
Environment variables names only — never values
| Var | Used by |
|---|---|
SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, SUPABASE_PROJECT_ID | Server Supabase clients / auth middleware |
VITE_SUPABASE_URL, VITE_SUPABASE_PUBLISHABLE_KEY, VITE_SUPABASE_PROJECT_ID | Browser Supabase client |
SUPABASE_SERVICE_ROLE_KEY | Server-side admin client (webhooks, queue drainer, cron hook) |
VITE_PAYMENTS_CLIENT_TOKEN | Stripe embedded checkout (client) |
STRIPE_SANDBOX_API_KEY, STRIPE_LIVE_API_KEY | Stripe server client (per env) |
PAYMENTS_SANDBOX_WEBHOOK_SECRET, PAYMENTS_LIVE_WEBHOOK_SECRET | Stripe webhook signature verification |
LOVABLE_API_KEY, LOVABLE_SEND_URL | Lovable gateway (Stripe proxy) + email send |
CRON_HOOK_SECRET | Bearer auth for the editor-due-soon pg_cron hook (also in Supabase Vault) |
The Supabase client throws "Connect Supabase in Lovable Cloud" if
VITE_SUPABASE_*are missing — env is Lovable-provisioned.
★ Appendix · Gotchas ▶
- Lovable coupling runs deep but narrow — only 4 app files import
@lovable.dev/*, yet auth, email transport, env injection, and the Vite plugin all flow through them. The authorization model (has_role(), ~36 RLS migrations, pgmq/pg_cron) is welded to a Supabase-issued JWT drivingauth.uid()— shedding Lovable packages is feasible; leaving Supabase is a security-model rewrite. - Dual lockfiles — use Bun, ignore
package-lock.json. - DB role naming — product says "client," the enum says
customer. Roles are rows inuser_roles, never a profile column; always check viahas_role(). - Assignment is admin-only — no auto-assignment exists. Dropping the admin role without first adding auto-assign/self-claim leaves orders unassigned with no due date.
- SSR error handling is custom —
src/server.tsexists to catch h3's swallowed{"unhandled":true}500s; preservenormalizeCatastrophicSsrResponse. - Generated files (
routeTree.gen.ts, Lovable/Supabase integration files,types.ts) won't survive regeneration — don't hand-edit.