feat(billing): per-seat foundation — org billing tables + seat gate (dormant) - #5117
Merged
Merged
Conversation
…dormant) Foundation for the per-seat subscription, fully dormant: everything sits behind STUDIO_BILLING_ENFORCED (default off; self-hosted never turns it on) and migration 137 backfills every existing org legacy = true, so nothing changes for anyone on deploy. - organization_billing (platform-written only — deliberately NOT in org metadata, which admins can write via ORGANIZATION_UPDATE) + backfill; new orgs get legacy = false from the org-creation hook, fail-OPEN when the row is missing. - organization_paid_seat: presence = paid; absence = free. Seats are monetization, orthogonal to Better Auth roles — an org OWNER can hold a free seat (the report-funnel onboarding case), so the gate fires BEFORE the admin/owner bypass in AccessControl.check(). - Free seat = sees everything, changes nothing: tools declaring readOnlyHint pass (fail-closed for unannotated ones), plus the ORG_FS_READ route resource key. - PaidSeatRequiredError extends ForbiddenError (existing handlers keep 403) with the [PAID_SEAT_REQUIRED] message prefix as the wire contract for the future paywall UI — same convention as [CREDITS]. - resolveOrgFromPath loads billing + seat (two PK lookups, members only, flag on only) and sets the gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
resolveOrgFromPath is the hot path for every org-scoped request. The billing legacy flag and paid-seat presence now ride the existing member query as scalar subqueries ($if-gated on STUDIO_BILLING_ENFORCED), so enforcement costs zero extra round-trips — and flag-off deployments pay exactly nothing, same as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
|
Pushed a hot-path refinement: the seat-gate reads (billing legacy + paid-seat presence) no longer add round-trips — they ride the existing membership query as scalar subqueries, |
Main took 137/138 (agent-sandbox); org billing is now migration 139. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
viktormarinho
enabled auto-merge (squash)
July 23, 2026 19:37
decocms Bot
pushed a commit
that referenced
this pull request
Jul 23, 2026
PR: #5117 feat(billing): per-seat foundation — org billing tables + seat gate (dormant) Bump type: minor - decocms (apps/mesh/package.json): 4.109.7 -> 4.110.0 Deploy-Scope: server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase-2 opener of the per-seat monetization plan: data model + enforcement seam, fully dormant — behind
STUDIO_BILLING_ENFORCED(default off, declared off in deco-apps-cd#175), with migration 139 backfilling every existing orglegacy = true. Deploying this changes behavior for no one.organization_billing(billing identity — platform-written only, deliberately NOT in orgmetadata, which org admins can write viaORGANIZATION_UPDATE; billing there would let any admin flag their own org legacy) +organization_paid_seat(presence = paid seat; absence = free). New orgs getlegacy = falsefrom the org-creation hook; a missing row fails open (treated legacy) so a hook failure can't brick an org.AccessControl.check()before the admin/owner bypass.annotations.readOnlyHint: truepass (fail-closed — an unannotated read tool gets annotated, not the gate widened); route-level resource keys have a read allowlist (ORG_FS_READ, notORG_FS_WRITE).PaidSeatRequiredErrorextendsForbiddenError(every existing handler keeps returning 403 untouched) and carries the[PAID_SEAT_REQUIRED]message prefix as the wire contract — same convention as[CREDITS]/is-credit-error.ts, so the future paywall UI distinguishes it from a plain permission 403 across every transport (REST, MCP tool error). CTA branching (subscribe vs "ask your admin") uses the role the frontend already has.resolveOrgFromPathloads billing + seat state (two PK lookups, members only, flag on only) and arms the gate for the request.Testing
access-control.test.ts(32 pass): owner-with-free-seat blocked,readOnlyHintpasses, basic-usage tools gated,ORG_FS_READpasses /ORG_FS_WRITEblocked, gate-off default fully open,ForbiddenErrorinheritance + prefix contract.bun testgreen;bun run check(all workspaces) green;bun run lintno new warnings;knipclean;bun run fmtapplied.🤖 Generated with Claude Code
Summary by cubic
Lays the foundation for per-seat billing, fully dormant behind
STUDIO_BILLING_ENFORCED(default off). Adds org billing tables and a seat gate in access control without changing behavior for existing users.New Features
annotations.readOnlyHint: trueand theORG_FS_READresource pass;ORG_FS_WRITEis blocked when gated.PaidSeatRequiredErrorextendsForbiddenErrorwith[PAID_SEAT_REQUIRED]prefix for paywall detection.resolveOrgFromPathfolds billing/seat into the member query as scalar subqueries whenbillingEnforcedis true; zero extra round-trips, and flag-off pays nothing.billingEnforced(fromSTUDIO_BILLING_ENFORCED); 6 unit tests cover the gate and error contract.Migration
organization_billing(billing identity) andorganization_paid_seat(presence = paid seat).legacy = true; new orgs getlegacy = falsevia the org-creation hook.Written for commit 1ef1edd. Summary will update on new commits.