type: meta

slug: plugin-backend-core

topic: Plugin-backend platform on Cloudflare Workers, assembled from @platform-modules; translation is first client service

exec_mode: run-plan-codex

scheduler: dag-parallel

base_branch: main

land_mode: pr

land_target: main

codex_impl_effort: high

status_enum: BLOCKED PENDING WORKING WRITTEN REVIEWED COMMITTED

truth_rule: this file is ground truth for INTENT (goals/directions/task list); git is ground truth for DONE-NESS — reconcile statuses against git, never trust a COMMITTED line with no commit

refresh_how: native Read of THIS file — NEVER ctx_execute/ctx_search

refresh_what: meta + every goal/methodology/intent/direction record + all WORKING and BLOCKED tasks (skip COMMITTED)

refresh_when:
wave boundary
any task status change
after a compaction system-reminder
before calling advisor

update_rule: on status change, rewrite that task's single line in place; append new tasks/session_memory as new lines

report_rule: at session end emit brief: done / blocked / deferred / canceled

note: run-plan-codex reads meta+task; promptbuilder pulls pinned contract from the .md anchor by (wave, positional order within wave) — task order in THIS file per wave MUST match the .md ### Task section order per wave. Launch: node /home/user/.claude/workflows/run-plan-codex.js --slug plugin-backend-core --repo /home/user/Projects/platform --concurrency 4


type: goal

text: Build a greenfield plugin-backend PLATFORM on Cloudflare Workers, assembled maximally from ~/Projects/platform @platform-modules, serving as shared backend (auth / API keys / subscriptions / billing / entitlements / ledger / invoicing) for ALL Press.Zone WordPress plugins. Translation is the FIRST client plugin service (deferred to a later spec).


type: methodology

text: Consume platform MODULES behind their seams; contribute genuine gaps UPSTREAM as new modules/subpaths, never as app-local islands. Contract-level plan (signatures/seams, no bodies). DB-backed tests use real pglite (mirror packages/ledger/src/test-fixture.ts). Each package change ships a changeset + regenerates subpath exports. Execute wave-by-wave via run-plan-codex; land via PR to main.


type: intent

text: One robust, reusable backend so every Press.Zone plugin gets auth/billing/entitlements/metering for free — 'our app is a lego piece of cake'. Avoid bespoke re-implementations that rot.


type: direction

verbatim: use ~/Projects/platform/ modules as much as possible for this project, do not write anythign from 0 if you dont have to. also, suggest new modules if needed, our app is going to be a lego piece of cake


verbatim: when building new modules for platform-modules, you must pass to the agents to laod the platform's CLAUDE.md, skills, memories, etc,


verbatim: go with Option A. run-plan-codex.js


text: Do NOT template the app off any platform app (mod-cms/mod-storefront/consumer are distros, irrelevant). Build apps/press-zone as its own Hono scaffold per platform coding-standard; consume MODULES only, behind seams.

verbatim: what does mod-cms have to do with any of it? mod-cms is a distro... nothing to do with our project


note: Codex plan schema/template = docs/plans/2026-07-03-mcp-agent-bearer.jsonl (+ .md). Tasks use title+acceptance+files_create+files_modify+blocks+blocked_by. NO gate0_cmd (worktrees have no node_modules → gate0 auto-skips; codex implementer installs + runs the acceptance turbo command itself). Waves are 1-based (run-plan-codex does wave||1, collapsing 0→1). Deps strictly lower-wave so blocked_by are COMMITTED before a wave runs.


note: BLOCKER RESOLUTIONS: (1) DB test harness = @electric-sql/pglite + local src/test-fixture.ts createTestDb(), exemplar packages/ledger/src/{test-fixture,debit}.test.ts. (2) Changeset NOT hook-enforced (lefthook pre-push = pnpm gate + block-main-push only; changesets consumed by release.yml on main) — run won't halt, but each pkg task adds a changeset as release hygiene. (3) New subpaths MUST run pnpm gen:subpath-exports else check:subpath-exports (in verify→gate) fails.


note: Confirmed: entitlements + invoicing packages ABSENT → net-new. billing exports .,./stripe,./sumit (paypal/subscriptions new). auth exports .,./engine-custom,./engine-better-auth,./otp-email (api-keys/oauth-provider new). tax exports ./rates-table (taxability new). tenancy seam resolveCapabilities(RbacResolveInput):Promise<string[]>. App scope = @app/press-zone. auth-react + billing-react exist for SPA.


note: RECOMMENDED at launch: run waves 1-2 (module contributions, t1-t9) first, verify green+reviewed, THEN resume app waves — reduces blast radius on live-money code, catches contract errors before app builds on them. run-plan-codex resumes (COMMITTED tasks skipped on rerun). ~25 codex tasks may brush codex 5h/7d limits — resumable.


type: task

status: COMMITTED

title: entitlements package (NEW @platform-modules/entitlements): can/quota/setTier + drizzle table + typed errors

acceptance: pnpm turbo typecheck build test --filter=@platform-modules/entitlements --concurrency=4 exits 0, zero warnings; pglite tests (mirror ledger test-fixture): can true-live/false-missing/false-expired, quota arithmetic, setTier idempotent, expiry boundary


title: tax taxability seam (extend @platform-modules/tax ./taxability): resolveTaxability above rates-table

acceptance: pnpm turbo typecheck build test --filter=@platform-modules/tax --concurrency=4 exits 0, zero warnings; check:subpath-exports green; tests: IL→IL standard, IL→US zero_rated, treatment→rate via rates-table, exhaustiveness (never-check) over standard/zero_rated/exempt — exempt is a reserved v1-unreached treatment, no fabricated business rule for it


title: invoicing package core (NEW @platform-modules/invoicing): issueInvoice + InvoiceProvider port + DocumentSpec + idempotency + mock provider

acceptance: pnpm turbo typecheck build test --filter=@platform-modules/invoicing --concurrency=4 exits 0, zero warnings; pglite tests: issue via mock returns doc, same idempotencyKey → provider called once + identical result, provider error → ok:false + no record


title: billing/subscriptions subpath (NEW @platform-modules/billing ./subscriptions): SubscriptionProvider port + ProviderEvent settlement kind + ingestWebhook (posts money-audit via confirmSettlement, no IntentStore)

acceptance: pnpm turbo typecheck build test --filter=@platform-modules/billing --concurrency=4 exits 0, zero warnings; check:subpath-exports green; pglite tests: settlement webhook → event chargeKey=sub:period, double-ingest same charge → single ledger_entries row, createSubscription idempotent, ProviderEvent union typechecks


status: PENDING

title: auth/api-keys subpath (NEW @platform-modules/auth ./api-keys): issueApiKey/verifyApiKey Bearer prefix.secret, crypto.subtle hash, scopes, drizzle api_keys table

acceptance: pnpm turbo typecheck build test --filter=@platform-modules/auth --concurrency=4 exits 0, zero warnings; check:subpath-exports green; pglite tests: issue→verify roundtrip, wrong secret→null, unknown prefix→null, scopes preserved, stored secret ≠ raw


title: app scaffold (NEW @app/press-zone): Hono worker entry {fetch,scheduled,queue}, envelope http.ts, config, db client, rate-limit mw, health route, wrangler.toml bindings — per platform coding-standard, NOT off any apps/* distro

acceptance: pnpm turbo typecheck build test --filter=@app/press-zone --concurrency=4 exits 0, zero warnings; tests: ok/fail envelope shapes, bigint field in ok(data) serializes to decimal string (no throw), /health 200 {data:{ok:true}}, rate-limit allows N blocks N+1 (429 envelope), config throws on missing var


title: invoicing/morning adapter (@platform-modules/invoicing ./morning): MorningProvider implements InvoiceProvider, Green Invoice token exchange + create-document via fetch, creds {apiUser,apiPass,companyId}

acceptance: pnpm turbo typecheck build test --filter=@platform-modules/invoicing --concurrency=4 exits 0, zero warnings; check:subpath-exports green; tests (fetch mocked): token exchange→bearer on create, success maps response→doc, API error→typed error


title: billing/paypal adapter (@platform-modules/billing ./paypal): PaypalProvider implements PaymentProvider & SubscriptionProvider, parseWebhook sig-verify→ProviderEvent, createSubscription/cancel/get, refund — web-standard fetch/crypto

acceptance: pnpm turbo typecheck build test --filter=@platform-modules/billing --concurrency=4 exits 0, zero warnings; check:subpath-exports green; tests: valid sig→settlement event stable chargeKey, invalid sig→error no event, refund webhook→refund event, createSubscription round-trips


title: auth/oauth-provider subpath (@platform-modules/auth ./oauth-provider): authorize/exchangeToken/registerClient/getClient, PKCE S256, module-owned oauth_clients/codes/tokens tables

acceptance: pnpm turbo typecheck build test --filter=@platform-modules/auth --concurrency=4 exits 0, zero warnings; check:subpath-exports green; pglite tests: authorize→code, exchange valid verifier→token, wrong verifier→error, code single-use, plain rejected, registerClient/getClient roundtrip


title: app domain schema + migration (@app/press-zone): compose module-owned tables (entitlements, api_keys, oauth_*, ledger wallet+entries, subscriptions) + app-owned (accounts, plugins, subscription_packages, sites, credentials, seats/members); 0001_init.sql; period-key convention; declare app's @platform-modules/* deps (entitlements,invoicing,billing,auth,tax,ledger,tenancy,audit) as workspace:^ + @electric-sql/pglite in devDependencies (app's first pglite test; t14/t18/t25 inherit) in package.json — single home so Waves 4-5 importers inherit

acceptance: pnpm turbo typecheck build test --filter=@app/press-zone --concurrency=4 exits 0, zero warnings; pglite tests: migration applies clean, all module+app tables queryable, wallet-key helper formats <account>:<plugin>:<period>


title: mw/auth (@app/press-zone): requireAuth resolves principal from session/api-key (verifyApiKey) + tenancy resolveCapabilities; 401 envelope on failure

acceptance: pnpm turbo typecheck build test --filter=@app/press-zone --concurrency=4 exits 0, zero warnings; tests: valid bearer→principal, invalid→401, capabilities attached


title: mw/access (@app/press-zone): requirePluginAccess(feature) = entitlements.can false→402 PLUGIN_NOT_ENTITLED ∩ tenancy capability absent→403 FORBIDDEN

acceptance: pnpm turbo typecheck build test --filter=@app/press-zone --concurrency=4 exits 0, zero warnings; tests: entitled+capable→next, no entitlement→402, no capability→403


title: lib/credential (@app/press-zone): resolveCredential(account,site,provider) over credentials link table, api-key-backed; typed not-found

acceptance: pnpm turbo typecheck build test --filter=@app/press-zone --concurrency=4 exits 0, zero warnings; tests: resolve hit, miss→typed error


title: lib/wallet (@app/press-zone): periodKey <account>:<plugin>:<period>, seedPeriodWallet upsert walletBalances (host seeding, NOT appendEntry), debitCredits guarded UPDATE WHERE balance>=amount (race-safe)

acceptance: pnpm turbo typecheck build test --filter=@app/press-zone --concurrency=4 exits 0, zero warnings; pglite tests (mirror ledger debit.test): seed+debit ok, over-balance→false unchanged, concurrent double-debit of 1-credit balance → exactly one succeeds


title: lib/billing-doc (@app/press-zone): issueSettlementInvoice = tax.resolveTaxability → rate via rates-table → DocumentSpec → invoicing.issueInvoice(MorningProvider); idempotent on idempotencyKey

acceptance: pnpm turbo typecheck build test --filter=@app/press-zone --concurrency=4 exits 0, zero warnings; tests (mock Morning): domestic 18% line, export 0%, idempotencyKey dedup (no 2nd document)


title: routes/oauth (@app/press-zone): authorize/callback/token delegating to auth/oauth-provider, PKCE S256, persist site credential; envelope; does NOT edit src/index.ts

acceptance: pnpm turbo typecheck build test --filter=@app/press-zone --concurrency=4 exits 0, zero warnings; tests: authorize returns code, token exchange stores credential, bad PKCE→400 envelope


title: routes/subscribe (@app/press-zone): PaypalProvider.createSubscription → entitlements.setTier → seedPeriodWallet(first period allocation); idempotent per account+package

acceptance: pnpm turbo typecheck build test --filter=@app/press-zone --concurrency=4 exits 0, zero warnings; tests (mock PayPal): happy path sets tier + seeds wallet, idempotent repeat, provider error rolls back (no tier/wallet)


title: routes/webhooks (@app/press-zone): billing.ingestWebhook (module posts money-audit) → dispatch does ONLY credit-reset via NEW period wallet + issueSettlementInvoice; idempotent on chargeKey/period NEVER eventId, never re-posts money; refund→credit-note

acceptance: pnpm turbo typecheck build test --filter=@app/press-zone --concurrency=4 exits 0, zero warnings; pglite+mock tests: settlement seeds next-period wallet + one invoice (no money re-post), redeliver same chargeKey → no 2nd seed/invoice/ledger row, refund → one credit-note


title: routes/plugin (@app/press-zone): metered gate requireAuth→requirePluginAccess(feature)→debitCredits(currentPeriodKey,cost); debit false→402 INSUFFICIENT_CREDITS; downstream failure after debit→compensating refund

acceptance: pnpm turbo typecheck build test --filter=@app/press-zone --concurrency=4 exits 0, zero warnings; tests: happy path debits once, insufficient→402 no debit-of-record, downstream failure refunds


title: routes/account (@app/press-zone): customer dashboard API — subscriptions/sites/seats/members+roles/invoices/credit-balance scoped to authenticated account (tenancy); envelope

acceptance: pnpm turbo typecheck build test --filter=@app/press-zone --concurrency=4 exits 0, zero warnings; tests: list scoped to account, cross-account read→403, member role update path


title: routes/staff (@app/press-zone): staff-gated ops — adjust credits, refund (PaypalProvider.refund + credit-note), tier override (entitlements.setTier), audit read; every mutation audited

acceptance: pnpm turbo typecheck build test --filter=@app/press-zone --concurrency=4 exits 0, zero warnings; tests: non-staff→403, credit adjust audited, refund path


title: server mount + scheduled + queue + wrangler finalize (@app/press-zone): TWO mount groups — public (webhooks, oauth authorize/callback/token — platform IS the authz server, /token is a back-channel call with code+PKCE verifier, NO user session, must NOT sit behind requireAuth, health) NO requireAuth, authenticated (subscribe, plugin, account, staff) requireAuth→requirePluginAccess; rate-limit on all; scheduled()→cron.ts housekeeping; queue()→queue.ts consumer; finalize bindings

acceptance: pnpm turbo typecheck build test --filter=@app/press-zone --concurrency=4 exits 0, zero warnings; integration test drives health + one guarded route; unauthenticated webhook POST AND unauthenticated /oauth/token POST each reach handler (NOT 401) while authenticated-group route returns 401; unmounted route→404 envelope


title: customer dashboard SPA shell (@app/press-zone-web, own workspace member — glob is apps/* so nested apps/press-zone/web would NOT be a member): React+Vite consuming auth-react + billing-react; screens login/connect, subscribe/plan, credits+usage, invoices, sites/seats; real envelope-aware API calls; thin shell

acceptance: pnpm turbo build test --filter=@app/press-zone-web --concurrency=4 exits 0, zero warnings; render tests for login + credits screens with mocked API


title: staff SPA shell (@app/press-zone-web staff area): accounts search, credit adjust, refunds, audit log view; consumes staff routes; thin shell

acceptance: pnpm turbo build test --filter=@app/press-zone-web --concurrency=4 exits 0, zero warnings; render test for accounts + credit-adjust with mocked API


title: HTTP integration flows (@app/press-zone): connect, subscribe, webhook-idempotency (double-deliver→single effect), metered-debit (+insufficient 402), staff-refund→credit-note; driven via app.request() against mounted Hono app over pglite, mocked PayPal/Morning — NOT a browser tool (SPA render covered by t23-24)

acceptance: pnpm turbo test --filter=@app/press-zone --concurrency=4 green (integration suite included); five flows pass; webhook double-delivery asserts single wallet seed + single invoice + no extra ledger_entries