Platform Billing Admin
Implementation design for workspace-bound assisted sales
Status: approved design for implementation onstaging
Scope: internal billing operations for Pilot, Scale, and Enterprise
Audience: the implementation agent and the Driftless reviewers
Risk: P1 — financial entitlements and cross-workspace administration
1. Outcome
Driftless needs a small internal billing surface where authorized platform operators can:- find and inspect customer workspaces;
- generate a single workspace-bound Stripe Checkout for Pilot or Scale;
- copy, cancel, or reissue the pending payment link;
- see whether an offer is creating, pending, activated, expired, canceled, or failed;
- activate or remove Enterprise access after a separately verified contract or invoice;
- audit who performed every sensitive action.
checkout.session.completed event is never sufficient authority to grant a plan.
2. Current implementation to preserve
The implementation must start from the effective code onmain/staging, not from the older
Free/Pro/Team billing topic.
Current facts:
WorkspaceGuardis the globalAPP_GUARD.- Workspace membership roles are
owner,admin, andmember. PLATFORM_ADMIN_USER_IDSalready gates the manual workspace-plan seam.assertHumanSession()rejects agent, API-key, and OAuth principals for human-only operations.@AccountLevel()authenticates a human without requiring membership in a selected workspace.- Founder, Pilot, and Scale Price IDs are server configuration; arbitrary Price IDs are rejected.
- Checkout metadata is written by the server and includes the workspace and plan.
- Only
customer.subscription.createdandcustomer.subscription.updatedwith an allowlisted Price and anactiveortrialingstatus may grant access. checkout.session.completedis non-mutating and does not grant access.- Enterprise is rejected by Checkout and uses the manual plan seam.
- Stripe subscription events are already ordered defensively so an old subscription cannot revoke a newer active subscription.
- The Stripe client is constructed lazily. Do not move it into a module/provider constructor.
POST /workspaces/:slug/billing/checkout-sessioncurrently acceptspilotandscalefrom any owner/admin of that workspace when their Price IDs are configured. Hiding the buttons is not a security boundary. The assisted-sales implementation must close this route before Pilot or Scale Price IDs are enabled in Stripe Live.
3. Authorization model
3.1 Do not create a workspace super-admin role
Do not addplatform_admin, billing_admin, staff, or similar values to
workspace_members.role.
workspace_members describes authority inside one customer’s workspace. Platform authority spans
workspaces and is a separate security domain. Mixing them would create confusing inheritance and
make accidental customer privilege escalation more likely.
3.2 Initial source of truth
For this version, platform billing authority remains the typed environment allowlist:3.3 Capability discovery
Add an authenticated account-level endpoint:3.4 Future role management
Aplatform_staff table and owner-managed staff UI are explicitly out of scope. Introduce them only
when the team needs delegated platform roles beyond the small server-owned allowlist. Do not expand
this implementation preemptively.
4. Data model and migration
Create one entity and one forward migration:4.1 billing_offers
Do not persist:
- Stripe secret keys;
- webhook secrets;
- full card/customer payment details;
- the raw Checkout URL;
- unbounded error payloads;
- coupons or promotion codes supplied by customers.
4.2 Constraints and indexes
Required constraints/indexes:4.3 TypeORM registration
RegisterBillingOffer everywhere the repository requires:
libs/db/src/index.tsexport;libs/db/src/data-source.tsentity and migration arrays;apps/api/src/app.module.tsruntime entity array;apps/api/test/test-datasource.tstest entity array;- the billing module’s
TypeOrmModule.forFeature(...)list.
app.module.ts registration is a release-blocking failure even if unit tests
pass.
4.4 RLS and exposure
The table is server-internal. It must not be queried from the browser through the Supabase Data API. Do not grantanon or authenticated direct table access. Enable RLS as defense in depth with no
customer policies, or keep the table in the server-only access pattern already used by the API.
5. Backend module boundary
Keep business logic in the existing billing service layer. Do not add a new library or business logic to the dashboard. Suggested files:PlatformBillingController orchestrates only. PlatformBillingService owns offer state transitions,
Stripe session creation/expiration, workspace lookup, and audit calls.
Do not add an @Public() route.
6. Internal API contract
All routes are account-level and require a human platform billing administrator.6.1 List workspaces
limitdefaults to 25 and is capped at 50.- cursor pagination is required; no offset pagination.
- default ordering is
created_at DESC, id DESC. - search matches normalized workspace name or slug.
- soft-deleted workspaces are excluded by default.
- return only the fields needed by the UI.
- do not return private workspace settings, API keys, model keys, or encrypted fields.
lower(name) and lower(slug). Do not add trigram search until measurement shows it is
necessary.
Owner email is not a search requirement in this version because Driftless has no local user
directory containing verified Clerk emails. Do not introduce N+1 Clerk calls or a new directory
table merely to support the first version.
6.2 Create Pilot/Scale offer
pilot and scale. The client never submits a Price ID, amount,
currency, workspace slug, Stripe customer ID, metadata, success URL, or trial/coupon value.
Response:
checkout_url is returned only after creation or reissue. It is not stored in Postgres and should
not be logged.
6.3 List offers
6.4 Cancel pending offer
creating or pending offers may be canceled. Expire an open Stripe Checkout Session where
possible, then atomically transition the local offer to canceled. Canceling an offer must never
cancel an already active subscription.
6.5 Reissue offer
expired, canceled, or failed offers. It creates a new offer ID and
new Stripe Checkout Session; it never mutates an old terminal offer back to pending.
6.6 Enterprise activation
actionisactivateordeactivate.reasonis required and bounded.contract_referenceis optional, bounded, and must not contain a contract body or secret URL.- activation calls the existing typed
setPlan(..., 'commercial', ..., 'enterprise')seam. - deactivation must refuse if there is an active Stripe subscription that the billing portal should manage instead.
- every action writes an append-only audit record.
7. Stripe lifecycle
7.1 Offer creation
- Authenticate and assert human platform-billing authority.
- Load the workspace by server-side
workspace_id; reject missing or soft-deleted workspaces. - Reject workspaces already on Founder or Commercial unless the approved upgrade policy explicitly supports the transition. The first version supports Free -> Pilot/Scale only.
- Resolve the plan’s Price ID from typed server configuration.
- Retrieve the Stripe Price server-side and verify it is active, recurring monthly, in the expected currency, and attached to the expected product.
- Generate the offer UUID in the application.
- Insert
billing_offers(status='creating')in a short transaction. - Outside the transaction, create a Stripe Checkout Session using the idempotency key
billing-offer:<offerId>. - Put immutable server-owned metadata on both Checkout and Subscription:
- Update the offer to
pendingwith the Stripe session ID and expiration. - If Stripe creation fails, mark the offer
failedwith a bounded failure code. Never leave a DB transaction open during the network request.
7.2 Entitlement grant
Keepcheckout.session.completed non-authoritative.
On customer.subscription.created or customer.subscription.updated:
- Verify the Stripe webhook signature using the raw request body.
- Require subscription status
activeortrialing. - Resolve the actual subscription item Price ID and require an allowlisted Pilot/Scale Price.
- Require
billing_offer_id,workspace_id, plan, and commercial band metadata. - Load the offer by ID and require:
- matching workspace;
- matching plan;
- matching Price ID;
- status
pendingor an idempotent replay ofactivated; - matching Stripe Checkout/subscription relationship where available.
- In one short transaction:
- transition offer to
activatedexactly once; - record Stripe customer/subscription IDs;
- call the same workspace billing transition used by the existing webhook;
- preserve existing event-order protections;
- record the audit event.
- transition offer to
7.3 Subscription cancellation and replacement
Existing subscription event ordering remains authoritative:- canceling the current active subscription revokes the commercial plan;
- a stale cancellation for an older subscription cannot revoke a newer active subscription;
- an offer is historical after activation and is never reopened by later subscription events.
8. Close the customer bypass
Before Pilot or Scale Price IDs are configured in Live Mode:- change the existing workspace checkout route so a customer owner/admin can request only
founder; - reject
pilot,scale,enterprise, arbitrary strings, and Price IDs on that route; - create Pilot/Scale Checkout only through
PlatformBillingServicewith a persisted offer; - keep Founder promotion-code and 15-day trial behavior unchanged;
- keep the customer billing portal available for existing Stripe customers.
9. Audit contract
Reuse the existing append-onlyaudit_log table.
Actions:
- human actor Clerk user ID;
- affected workspace ID;
- offer ID or workspace slug as target;
- plan and previous/new status;
- Stripe object IDs where useful;
- bounded reason/contract reference for Enterprise.
billing.offer.activate uses a fixed platform actor such as stripe_webhook only if
the existing audit contract permits it; otherwise store the initiating offer creator in detail and
keep webhook provenance explicit. Do not pretend the webhook is a human.
10. Dashboard design
10.1 Placement
Reuse the existing Settings surface and primitives.- URL:
/w/:activeSlug/settings?section=platform-billing - Rail label:
Administración - Page title:
Billing de clientes - Visibility: only when
GET /me/platform-capabilitiesreturnsplatform_billing_admin: true.
10.2 Page structure
SettingsPageHeader, SettingsSection, SettingsCard, SettingsRow, buttons,
inputs, dialogs, badges, skeletons, empty states, and pager. Preserve the dashboard’s current visual
language.
10.3 Interaction rules
- Search is debounced 300 ms.
- Results are server-paginated at 25 rows.
- Search, plan, status, and cursor live in the URL query string.
- Loading preserves table dimensions; no full-page flashing.
- Empty state explains how to search or generate the first offer.
- Errors appear inline and remain actionable.
- Copy button confirms
Enlace copiadowithout exposing the URL elsewhere. - Generating a link requires a confirmation dialog showing workspace and exact monthly price.
- Enterprise activation requires typing/confirming the workspace slug plus a reason.
- Cancel/reissue actions require confirmation.
- Activated and terminal offers cannot show destructive actions that no longer apply.
- Keyboard navigation, focus return, labels, and status announcements are required.
- Mobile may stack each row into a compact card; no horizontal overflow that hides actions.
10.4 Offer detail drawer/dialog
Show:- workspace name and slug;
- selected plan and amount;
- status and expiration;
- created by and created time;
- Stripe session/subscription identifiers in copyable monospace text;
- audit timeline;
- copy, cancel, or reissue actions when allowed.
11. Performance contract
- Cursor pagination only; maximum 50 rows.
- Query only selected workspace columns plus a bounded latest/open-offer projection.
- Avoid N+1 offer or Clerk queries.
- Use one set-based query or a bounded two-query composition for the page.
- Index foreign keys and the exact status/order filters used by the list.
- Exclude soft-deleted workspaces in the query and index strategy.
- Search must remain responsive for at least 100,000 workspaces and 1,000,000 historical offers.
- Add an integration test for stable cursor ordering when multiple rows share
created_at.
12. Security test matrix
The implementation is incomplete unless automated tests cover every row:
Also run the architecture specs that enforce
WorkspaceGuard, OAuth default-deny, TypeORM entity
registration, and controller/service separation.
13. Error and state behavior
Use typed NestJS exceptions. Never throw rawError from services and never swallow a paid-event
database failure that Stripe should retry.
Suggested UI-safe errors:
14. Configuration
Typed and startup-validated configuration:- staging uses Stripe Sandbox Price IDs;
- production uses Stripe Live Price IDs only;
- never mix test and live Price IDs/keys;
- absence of platform billing configuration must not crash the entire API;
- internal endpoints return a typed unavailable response when disabled;
- do not log any secret or the full allowlist.
15. Rollout order
Phase A — staging implementation
- Add entity and migration.
- Register entity/migration in every runtime and test data source.
- Centralize platform billing authority.
- Add account-level capability and internal endpoints.
- Close the Pilot/Scale customer checkout bypass.
- Extend webhook validation to require a matching pending offer before Pilot/Scale activation.
- Add audit events.
- Add Settings UI and translations.
- Configure Ada/Mia Clerk IDs and Sandbox Price IDs in staging.
- Run migration and full harness.
Phase B — staging acceptance
- Non-admin cannot see the section and receives 403 when calling it directly.
- Ada/Mia can search/paginate workspaces.
- Generate a Sandbox Pilot offer for a disposable workspace.
- Pay with a Stripe test card.
- Confirm the exact workspace becomes Commercial/Pilot.
- Confirm another workspace remains unchanged.
- Cancel and reissue a Scale offer.
- Exercise Enterprise activation/deactivation with audit readback.
- Verify mobile and desktop UI.
- Verify no Checkout URL, promotion code, or secret appears in logs.
Phase C — production activation
Production activation is a separate explicit release:- Create/copy Pilot and Scale products/prices in Stripe Live.
- Configure Live Price IDs in Render production.
- Configure the production Stripe webhook events/secrets.
- Enable platform billing for the approved operator IDs.
- Deploy the already-reviewed code.
- Generate one controlled Live offer.
- Complete, refund/cancel if appropriate, and reconcile it end to end.
16. Rollback
Code rollback:- set
PLATFORM_BILLING_ADMIN_ENABLED=falseto disable the internal surface; - keep Founder self-serve unaffected;
- revert the application change if needed.
- expire pending Checkout Sessions;
- mark corresponding local offers canceled;
- archive/deactivate Pilot and Scale Live Prices only after verifying no active subscriptions depend on them;
- manage paid subscriptions through Stripe Billing Portal/Stripe Dashboard, not by deleting rows.
billing_offers only before any real offer exists. Once financial
activity exists, preserve the table and disable the feature instead of destroying history.
17. Definition of done
The cloud agent may report completion only when all are true:- one forward migration creates the constrained/indexed
billing_offerstable; - runtime and test TypeORM registrations are complete;
- no workspace membership role was expanded;
- only human allowlisted platform admins reach the internal routes;
- customers cannot request Pilot/Scale Checkout directly;
- every Pilot/Scale Checkout has a persisted workspace-bound offer;
- only an active/trialing allowlisted subscription with a matching pending offer grants access;
- Enterprise is manual, human-only, reasoned, and audited;
- Settings shows the section only for the server-returned capability;
- list/search/filter/pagination are server-side and responsive;
- all negative security cases are tested;
- focused billing/auth/dashboard tests pass;
- API and dashboard builds/typechecks pass;
bash scripts/harness/check.shpasses with zero failures;- staging migration and an end-to-end Sandbox Checkout are verified;
driftless context get --diffis reviewed and durable context is updated/proposed correctly.
18. Expected implementation handoff
The cloud agent should deliver:- a PR to
stagingwith narrow commits; - migration and rollback notes;
- API and UI screenshots/evidence;
- test and harness output;
- a security matrix with pass/fail evidence;
- the staging Checkout session/subscription/workspace IDs used for verification, excluding secrets;
- explicit confirmation that Stripe Live and production were not touched.
