Skip to content

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.


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.


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.


DomainPlaneOwner svcResponsibility
CatalogFoundationSupabaseseries / issues / pages / panels metadata; publish state; ratings; locale.
Identity & EntitlementsbothSupabase Authone account across reader/buyer/licensee; free access always granted; premium entitlements (canon §6.3).
Reading ProgressFoundationSupabaseresumable per-user progress; offline sync; RLS owner-only.
Media / CPF Ingest PipelineFoundationRust axumupload → validate CPF → virus/content scan → image tiling/transcode → R2 → publish (canon §6.1).
Rights & Royalty LEDGERbridgeRust axumsource of truth for ownership + money owed; computes royalties; feeds creator dashboards (canon §6.4/§6.5).
Licensing & PartnersVariantRust axumlicensee onboarding, grants, sublicense terms, partner portal (canon §6.6).
CommerceVariantRust + StripeVARIANT EDITION store, checkout, fulfillment hooks → royalty events.
DonationsFoundationStripecharitable gifts (separate Stripe account); receipts; no entitlement granted.
SearchFoundationPostgres FTScatalog discovery; later a dedicated index (canon §4).
Analytics / Eventsbothevent pipelineprivacy-respecting reading/engagement events (canon §6.7); no ad trackers; COPPA-aware.
Notificationsbothworkertransactional email/push (new issue, payout, license expiry).
Admin / ReviewbothSupabase + svceditorial publish workflow, content rating, moderation, UBIT/finance ops views.

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 atom
right(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 + partners
royalty_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.5

royalty_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.

  • reading_progress, entitlement, donation: owner-only (account_id = auth.uid()).
  • series/issue/page/panel: public read when status='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 prefix
2 validate CPF schema_version check; required fields; panel reading-path integrity; alt-text presence
3 scan virus scan + content scan (rating sanity, prohibited content) -> quarantine on fail
4 tile/xcode generate deep-zoom tiles + responsive derivatives + transcodes; checksum each
5 store write immutable, content-hashed objects to R2 (prod prefix); record `asset` rows
6 publish mark cpf_manifest.validated=true, flip issue.status='published'; CDN purge/warm

Idempotent & 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 → payout graph.
  • Compute royalties from COMMERCE checkouts, sublicense reports, and partner usage reports: on each money event, resolve applicable royalty_terms, compute computed_amount server-side, write an append-only royalty_event keyed by idempotency_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.


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_cursor in 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_id referencing a published catalog issue, and (b) the inter_entity_license grant from 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.

  • 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: immutable on 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/*_KEY client-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.

  1. 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.
  2. Identity + progress: Supabase Auth signup; resumable progress survives device switch; RLS verified (user cannot read another user’s progress/entitlements).
  3. Pipeline correctness: upload → validate (rejects bad CPF) → scan → tile → R2 → publish is idempotent (re-run = no-op) and versioned (rollback to prior manifest works).
  4. LEDGER end-to-end: a VARIANT EDITION purchase via Stripe writes one idempotent royalty_event, aggregates into a payout, and surfaces in a creator’s scoped royalty view — amounts computed server-side, append-only, reconciling to the Stripe charge.
  5. Boundary enforced: VARIANT credentials cannot write catalog/progress; FOUNDATION credentials cannot read raw ledger.*; both proven by an authz test.
  6. Donations vs commerce: donation flows to the FOUNDATION Stripe account, grants no entitlement; purchase flows to the VARIANT account and does.
  7. 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 %.