Redpoint Engine — Architecture
1. Shape
One Next.js app + one Postgres (Supabase) + R2 + Stripe. Three surfaces served by the same app:
- Engine UI — ops console (admin), editor workspace, client review pages. Tenant-branded via theme tokens.
- Engine API (
/api/v1/*) — the headless surface external tenant frontends use: create order, get upload URLs, read job status, approve/revise. Auth: tenant API keys (server-to-server) or the client's Supabase session. - Webhooks out — signed events (
job.status_changed,draft.ready,job.delivered) to each tenant's registered endpoint, so external frontends can react without polling.
Tenant frontends (e.g. wakecut.com marketing + order pages) are separate thin apps. They call the Engine API and embed/redirect to engine-hosted flows (upload, review) to avoid rebuilding the hard parts. In-house tenants may also just use engine-hosted pages on their domain.
2. Tenancy model
tenants— slug, domains[], theme jsonb, billing_config jsonb, retention_days, platform_fee_bps, email sender identity, active.- Tenant resolution: middleware maps request host → tenant (fallback:
?tenant=in dev). Tenant context flows via request headers into server components. - Demand is tenant-scoped; supply is platform-scoped. Clients belong to exactly one tenant (
profiles.tenant_id). Editors and admins are platform-level (tenant_id null). Editor↔tenant eligibility lives ineditor_tenants(per-vertical test-edit approval). - Per-tenant config, never code: brief templates (
brief_templates, versioned jsonb schema), package catalogue, billing modes, theme, email templates.
3. Roles
client— belongs to a tenant; creates orders, uploads, reviews, approves. Sees only own jobs.editor— platform-level, pseudonymous to clients; sees only assigned jobs; downloads originals, uploads drafts.admin— Redpoint operator; sees everything across tenants; assigns, QCs, manages payouts, refunds.- (
tenant_admin— reserved for licensed partners later; column enum already includes it, no UI yet.)
4. Job state machine (unchanged spine)
NEW → ASSIGNED → IN_PROGRESS → IN_REVIEW → (QC_REJECTED ↩ IN_PROGRESS)
→ CLIENT_REVIEW → (REVISION ↩ IN_PROGRESS, capped) → APPROVED → DELIVERED
any-pre-capture → CANCELLED
Status changes are audit-logged to events by trigger. Allowed transitions enforced in a single server-side transition function (transitionJob()) — never raw updates from UI code.
5. Money flow
- Order → Stripe Checkout, manual capture (authorize only). Billing mode per tenant config:
one_off: one Checkout per job.retainer: Stripe subscription grants N job entitlements/month (entitlementstable); job creation debits one, no per-job checkout.credits: prepaid pack purchase creditsentitlements; same debit path as retainer.rush: surcharge line item, flag on job, tighter SLA.
- Webhook (verified) creates job
NEWwithpayment_intent_id(or debits entitlement). - Client APPROVED → capture → Stripe Connect transfer of editor share (
packages.editor_payout_*) → margin retained. - CANCELLED pre-capture → void authorization.
- VAT: record
buyer_country+buyer_is_businessper job; no tax computation yet.
6. Files
- Uppy in browser → presigned multipart R2 upload (resumable, multi-GB, throttled-connection safe). Server issues URLs; bytes never transit the server.
- Remote ingest (docs/INGEST-SPEC.md): clients can instead connect a service or paste a link — Tier 1 official APIs via self-hosted Uppy Companion (Dropbox, Google Drive, Google Photos, OneDrive, Box, direct URL), Tier 2 feature-flagged resolvers for WeTransfer/SwissTransfer/iCloud shared links (unofficial), Tier 3 ops-assisted manual queue. All paths land identical
filesrows (provenance viaingests+files.ingest_id); runs on a small always-on EU ingest service, later shared with the ffmpeg worker. filesrows: kindoriginal | draft | preview | final, r2_key, size, uploader.- Drafts shown to clients as watermarked previews (Phase 9 transcoder; until then, editors upload a watermarked preview export alongside the draft — process rule, then automated).
- Finals released (signed GET URLs) only at APPROVED/capture.
- Retention: cron deletes originals
retention_days(default 45) after DELIVERED; erasure action deletes job + R2 objects + anonymises events.
7. Dispatch (launch = managed; pool-ready schema)
Launch behaviour: admin assigns editor + internal deadline manually. Schema already carries what stage 2 (curated pool) needs so it becomes a feature flag, not a migration: editor_profiles (skill tags, max_concurrent_jobs, available bool, vetting_status: applied→test_assigned→approved), editor_tenants eligibility, quality counters (qc_pass/revision/on_time rates) updated by triggers. Auto-match = filter eligible+available+under-cap, rank by quality — Phase 8+, admin override always.
8. Security
- RLS everywhere; tenant isolation on every tenant-scoped table (
tenant_idcheck via profile), participant checks on jobs/files/events. - Editors never see client identity beyond first name/brand needed for the brief; clients never see editor identity (pseudonym only).
- R2 access only via short-lived signed URLs scoped per file.
- Tenant API keys hashed at rest; webhook payloads HMAC-signed.
- Events table = audit trail (GDPR data-subject requests, disputes).
9. Non-goals (unchanged from original spec)
No open bidding, no native apps, no custom timestamped review player (Frame.io fills the gap early), no auto-pricing of bespoke edits, no unlimited-subscription billing.