OPEN PANEL — Backend Spec (`SPEC_BACKEND.md`)
OPEN PANEL — Backend Spec (SPEC_BACKEND.md)
Section titled “OPEN PANEL — Backend Spec (SPEC_BACKEND.md)”Derives from
CANON.md(single source of truth). Where this doc and canon disagree, canon wins. Stack is locked per canon §4. Status: DRAFT v0.1 — not legal/financial advice. Brand/product nouns are ALL-UPPERCASE in prose; lowercase in code/paths/identifiers.
1. Scope & guiding principle
Section titled “1. Scope & guiding principle”The backend serves two planes over one identity (canon §3):
- FOUNDATION product plane — the free reading product. Catalog, reader payloads, accounts, progress, the CPF media pipeline, donations. Optimized for free-at-scale reads (CDN-first).
- VARIANT commerce/licensing plane — the for-profit surface. Store, partner/licensee management, premium entitlements, and the Rights & Royalty Ledger (canon §6.4) that is the source of truth for who-owns-what and money owed.
The planes are separated yet linked: the only sanctioned bridge between them is the LEDGER and the inter-entity license (canon §6.6). FOUNDATION data never depends on VARIANT data; VARIANT reads FOUNDATION catalog identifiers and writes royalty/commerce events that flow up to the LEDGER.
2. Service architecture (ASCII)
Section titled “2. Service architecture (ASCII)” CLIENTS (web / mobile / desktop — @openpanel/reader) | HTTPS (JWT, Supabase Auth) v +-----------------------------------+ | API GATEWAY / edge | rate limit, authz, routing, | (CDN edge + Supabase API + axum) | response cache for free reads +-----------------+-----------------+ | +------------------------+-------------------------+ | | == FOUNDATION PRODUCT PLANE == == VARIANT COMMERCE/LICENSING PLANE == | | +--------v---------+ +------------------+ +----------v-----------+ +-----------------+ | Supabase | | Rust (axum) | | Rust (axum) | | Rust (axum) | | - Auth/Identity | | media/CPF ingest | | Rights & Royalty | | Licensing & | | - Catalog (PG) | | pipeline (tile/ | | LEDGER (money truth) | | Partners portal | | - Entitlements | | scan/validate) | +----------+-----------+ +--------+--------+ | - Progress (RLS) | +---------+--------+ | | | - Storage(meta) | | | | | - FTS (search) | | +---------v-----------------------v------+ +--------+---------+ | | Commerce (Stripe) | Donations (Stripe) | | | +----------------------------------------+ | | | +-----------+-----------+----------------------------------+ v v v +---------------+ +------------------+ +---------------+ | POSTGRES | | CLOUDFLARE R2 | | STRIPE | | (Supabase + | | + CDN | | (2 accounts: | | ledger DB) | | tiled assets,CPF | | Variant store,| +---------------+ +------------------+ | Foundation | | donations) | +---------------+Note: the LEDGER may run its own logical Postgres database/schema (ledger) with restricted
service-role access, distinct from the Supabase app schema. Reads of free comics short-circuit at
the CDN edge and never touch a Rust service.
3. Domains & responsibilities
Section titled “3. Domains & responsibilities”| Domain | Plane | Owner svc | Responsibility |
|---|---|---|---|
| Catalog | Foundation | Supabase | series / issues / pages / panels metadata; publish state; ratings; locale. |
| Identity & Entitlements | both | Supabase Auth | one account across reader/buyer/licensee; free access always granted; premium entitlements (canon §6.3). |
| Reading Progress | Foundation | Supabase | resumable per-user progress; offline sync; RLS owner-only. |
| Media / CPF Ingest Pipeline | Foundation | Rust axum | upload → validate CPF → virus/content scan → image tiling/transcode → R2 → publish (canon §6.1). |
| Rights & Royalty LEDGER | bridge | Rust axum | source of truth for ownership + money owed; computes royalties; feeds creator dashboards (canon §6.4/§6.5). |
| Licensing & Partners | Variant | Rust axum | licensee onboarding, grants, sublicense terms, partner portal (canon §6.6). |
| Commerce | Variant | Rust + Stripe | VARIANT EDITION store, checkout, fulfillment hooks → royalty events. |
| Donations | Foundation | Stripe | charitable gifts (separate Stripe account); receipts; no entitlement granted. |
| Search | Foundation | Postgres FTS | catalog discovery; later a dedicated index (canon §4). |
| Analytics / Events | both | event pipeline | privacy-respecting reading/engagement events (canon §6.7); no ad trackers; COPPA-aware. |
| Notifications | both | worker | transactional email/push (new issue, payout, license expiry). |
| Admin / Review | both | Supabase + svc | editorial publish workflow, content rating, moderation, UBIT/finance ops views. |
4. Data model sketch (Postgres)
Section titled “4. Data model sketch (Postgres)”Conventions: uuid PKs, created_at/updated_at timestamptz, soft-delete via deleted_at,
money as integer minor units + ISO currency. RLS posture in §4.3.
4.1 Foundation product schema (public, Supabase)
Section titled “4.1 Foundation product schema (public, Supabase)”series(id, slug, title, synopsis, locale_default, rating, status, cover_asset_id, created_at)issue(id, series_id->series, number, title, status{draft|in_review|published}, cpf_manifest_id->cpf_manifest, release_at, is_premium bool, variant_edition_id NULL, created_at)page(id, issue_id->issue, index, asset_id->asset, width, height, alt_text)panel(id, page_id->page, order, region jsonb /* guided-view bbox */, alt_text, reading_path_index)asset(id, r2_key, kind{page|tile|cover|cpf}, content_hash, bytes, mime, version)cpf_manifest(id, issue_id, version, schema_version, validated bool, manifest jsonb, created_at)
account(id == auth.users.id, display_name, role{reader|creator|partner|admin}, dob_band, created_at)entitlement(id, account_id->account, scope{free|premium}, issue_id NULL, series_id NULL, source{always|purchase|grant}, stripe_ref NULL, granted_at, expires_at NULL)reading_progress(id, account_id->account, issue_id->issue, page_index, panel_index, percent, updated_at, device_id) -- UNIQUE(account_id, issue_id)donation(id, account_id NULL, stripe_payment_intent, amount, currency, receipt_url, created_at)4.2 Rights & Royalty LEDGER schema (ledger, restricted) — canon §6.4
Section titled “4.2 Rights & Royalty LEDGER schema (ledger, restricted) — canon §6.4”work(id, issue_id /* link to catalog */, title, created_at) -- the IP atomright(id, work_id->work, type{print|merch|adaptation|translation|digital}, territory, scope jsonb)rights_holder(id, kind{foundation|creator|variant}, account_id NULL, legal_name)grant(id, right_id->right, grantor_id->rights_holder, grantee_id->rights_holder, basis{inter_entity_license|creator_agreement|sublicense}, starts_at, ends_at, terms_doc_url)licensee(id, rights_holder_id->rights_holder, party_name, contact, status) -- Variant + partnersroyalty_term(id, grant_id->grant, rate_bps, base{net_revenue|units}, min_guarantee, payee_id->rights_holder)royalty_event(id, royalty_term_id->royalty_term, source{commerce|sublicense|partner_report}, source_ref, gross, net, computed_amount, currency, period, idempotency_key UNIQUE, created_at)payout(id, payee_id->rights_holder, period, amount, currency, status{pending|paid|failed}, stripe_transfer_ref, royalty_event_ids uuid[], created_at)creator_agreement(id, work_id, creator_id->rights_holder, share_bps, model{wfh|coown|hybrid}, machine_terms jsonb, signed_at) -- canon §5/§6.5royalty_event is append-only; computed_amount is derived (never client-supplied).
The FOUNDATION→VARIANT master license (canon §6.6) is one grant row of basis
inter_entity_license from which VARIANT sublicenses descend.
4.3 RLS posture
Section titled “4.3 RLS posture”reading_progress,entitlement,donation: owner-only (account_id = auth.uid()).series/issue/page/panel: public read whenstatus='published'; writes service-role only.ledger.*: no anon/auth access — service-role only via the LEDGER axum service; creators read their own royalties through a scoped read API, never direct table access.
5. Media / CPF ingest pipeline (canon §6.1)
Section titled “5. Media / CPF ingest pipeline (canon §6.1)”1 upload creator/admin uploads source pages + draft CPF manifest -> staging R2 prefix2 validate CPF schema_version check; required fields; panel reading-path integrity; alt-text presence3 scan virus scan + content scan (rating sanity, prohibited content) -> quarantine on fail4 tile/xcode generate deep-zoom tiles + responsive derivatives + transcodes; checksum each5 store write immutable, content-hashed objects to R2 (prod prefix); record `asset` rows6 publish mark cpf_manifest.validated=true, flip issue.status='published'; CDN purge/warmIdempotent & versioned: every step keyed by (issue_id, manifest version, content_hash); re-running
produces the same R2 keys and is a no-op if hashes match. Failed runs leave prod untouched (staging
only). A new manifest version = new immutable asset set; the previous version stays addressable for
rollback. Pipeline is a Rust axum service + worker queue (heavy CPU = why it is Rust, not Supabase fn).
6. Rights & Royalty LEDGER service (canon §6.4)
Section titled “6. Rights & Royalty LEDGER service (canon §6.4)”The single source of truth for who-owns-what and money owed. It is a Rust (axum) service because money correctness demands strong typing, transactional integrity, deterministic decimal math, and an auditable append-only event log — risks too high for ad-hoc app code.
Responsibilities:
- Hold the canonical
work → right → grant → licensee → royalty_term → royalty_event → payoutgraph. - Compute royalties from COMMERCE checkouts, sublicense reports, and partner usage reports:
on each money event, resolve applicable
royalty_terms, computecomputed_amountserver-side, write an append-onlyroyalty_eventkeyed byidempotency_key(Stripe event id / report id). - Aggregate into payouts per payee per period; emit Stripe transfers; reconcile.
- Feed creator dashboards (canon §5) and FOUNDATION UBIT monitoring / reinvestment reporting (canon §7.4/§7.10) — royalties up to the FOUNDATION must read as passive (canon §2).
Invariants: events are immutable; recompute = new correcting event, never an in-place edit; every
payout cites the royalty_event_ids it settled; all amounts in integer minor units.
7. API surface
Section titled “7. API surface”Choice: REST + typed schema (OpenAPI). Rationale: CDN-friendly cacheable GETs for free reads, simpler edge caching and rate limiting than GraphQL, and typed clients generated for the shared TS reader. (GraphQL reconsidered only if client over/under-fetching becomes a real problem.)
- Versioning: URL prefix
/v1/...; additive changes in-place, breaking changes →/v2. - Auth: JWT via Supabase Auth on every authed call; service-to-service via signed service tokens.
- Pagination: cursor-based (
?cursor=&limit=), stable sort keys;Link/next_cursorin response. - Rate limiting: per-IP at edge for anon reads; per-account token bucket for writes; stricter buckets on commerce/licensing and ledger endpoints.
Representative endpoints:
GET /v1/catalog/series (public, CDN-cached)GET /v1/catalog/issues/{id} (public; CPF/manifest pointer)GET /v1/issues/{id}/cpf (public for free; entitlement-checked if premium)GET /v1/me/progress PUT /v1/me/progress (owner-only)GET /v1/me/entitlements (owner-only)POST /v1/commerce/checkout (Variant; Stripe session)POST /v1/webhooks/stripe (idempotent; ledger ingest)POST /v1/licensing/grants GET /v1/licensing/licensees (Variant, partner/admin scope)GET /v1/ledger/creators/{id}/royalties (scoped: own data only)GET /v1/search?q= (FTS)POST /v1/events (analytics, batched)8. Inter-entity boundary (FOUNDATION ↔ VARIANT)
Section titled “8. Inter-entity boundary (FOUNDATION ↔ VARIANT)”- Separation: FOUNDATION product data (
public) and LEDGER (ledger) are distinct schemas with distinct service-role credentials. VARIANT services hold no write access to catalog/progress. - Linkage: the only links are (a)
work.issue_idreferencing a published catalog issue, and (b) theinter_entity_licensegrantfrom which VARIANT sublicenses + royalties descend (canon §2/§6.6). - Flow: COMMERCE/partner money events → LEDGER computes royalty_events → payouts → royalties up to FOUNDATION read as passive (canon §2, §512(b)(2) posture). Premium entitlements are written by COMMERCE but read by the FOUNDATION reader via the entitlements contract (canon §6.3).
- Access control: governance independence (canon §2/§7.1) is mirrored technically — separate credentials, audit logging on every inter-plane call, no shared service token across planes.
9. Non-functional requirements
Section titled “9. Non-functional requirements”- Scalability (free-at-scale reads): catalog + CPF + tiles are CDN-first; published reads are immutable, content-hashed, cache-forever-with-versioned-keys. Origin services scale for writes/commerce/ledger, not for the read fan-out.
- Caching: edge cache for public catalog/assets;
Cache-Control: immutableon versioned assets; short TTL + revalidation on mutable catalog listings; purge-on-publish. - Observability: structured logs, request tracing across gateway→services, ledger has a dedicated financial audit log; metrics on pipeline throughput, cache hit ratio, payout reconciliation.
- Backups / DR: Supabase PITR; ledger DB on stricter backup cadence + tested restore; R2 versioning + lifecycle; documented RTO/RPO (ledger RPO ≈ 0 target).
- Security: secrets in a manager (never client; canon §0 rule — no
*_SECRET/*_KEYclient-side); service-role keys server-only; signed webhooks; least-privilege per service. - PII / COPPA (canon §7.7): comics skew young — data minimization, age-band not exact DOB where possible, no behavioral ad tracking, parental-consent gating where required, GDPR delete/export.
- Idempotency: all money + pipeline + webhook operations carry idempotency keys; safe retries.
10. Acceptance criteria / MVP slice
Section titled “10. Acceptance criteria / MVP slice”- Catalog + free read: publish a series/issue via pipeline; anon user reads it from CDN with a valid CPF manifest (page + guided view); no Rust service hit on the read path.
- Identity + progress: Supabase Auth signup; resumable progress survives device switch; RLS verified (user cannot read another user’s progress/entitlements).
- Pipeline correctness: upload → validate (rejects bad CPF) → scan → tile → R2 → publish is idempotent (re-run = no-op) and versioned (rollback to prior manifest works).
- LEDGER end-to-end: a VARIANT EDITION purchase via Stripe writes one idempotent
royalty_event, aggregates into apayout, and surfaces in a creator’s scoped royalty view — amounts computed server-side, append-only, reconciling to the Stripe charge. - Boundary enforced: VARIANT credentials cannot write catalog/progress; FOUNDATION credentials
cannot read raw
ledger.*; both proven by an authz test. - Donations vs commerce: donation flows to the FOUNDATION Stripe account, grants no entitlement; purchase flows to the VARIANT account and does.
- Search: Postgres FTS returns published catalog matches with cursor pagination.
Open items deferred to canon §10 interview: jurisdiction, entity relationship (Option A/B affects whether ledger payouts are arm’s-length royalties vs. intra-group), and creator share %.