Corteksa
GuidesBilling

Backend Integration

Billing — Backend Integration

For backend developers extending or maintaining Billing. All billing state lives in the MAIN database, keyed by user_workspace_id.

Services

ServiceJob
BillingCheckoutServicechangePlan (plan change), createCheckout (opens the hosted checkout / sets up the subscription), cancelSubscription, getPaymentStatus. Controllers call this.
SubscriptionServiceSubscription lifecycle: processWebhookEvent, trial/grace transitions, getPaymentHistory.
BillingProrationServiceapplySeatChange + plan-change proration (difference-based).
BillingPricingServiceResolves plan price / seat totals for a charge.
BillingEnforcementServiceenterReadOnly / exitReadOnly; enrolls & cancels billing-initiated deletion.
BillingAddOnServicelistCatalog, purchaseAddOn, cancelRecurringAddOn.
BillingInvoiceServicegenerateInvoicePdf (branded PDF via Puppeteer).
UsageOverviewServiceBuilds the GET /usage payload.

The payment gateway is injected by token (PAYMENT_GATEWAY_TOKENPaymobGatewayService, aliased to IPaymentGateway). Swap gateways by implementing that interface and replacing the three Paymob providers + PaymobWebhookController — the subscription services need no changes.

UsageModule (importable stand-alone)

billing/usage/ is a self-contained metering module (MAIN db) that other feature modules import without the full BillingModule. Exports: UsageLimitResolverService, UsageCounterService, WorkspaceIdentityService.

  • Gate before / record after. ensureAiAllowance (throws 402) then recordAiCredits (post-hoc real cost); hasWorkflowAllowance (peek) then recordWorkflowExecution (never throws); checkStorage / addStorageBytes.
  • Counters are keyed by month (period_key = YYYY-MM); storage uses the cumulative key *. Overflow beyond the plan limit draws from one-time pack balances (workspace_consumables).

Controllers & guards

ControllerPathGuardNotes
BillingController/user/billingUserJwtAuthGuard@AllowWhenReadOnly() — stays writable while read-only so the customer can pay.
BillingAddOnController/user/billing/addonsUserJwtAuthGuard@AllowWhenReadOnly().
PaymobWebhookController/webhooks/paymobPaymobWebhookGuardPublic; @SkipUserJwt/@SkipAdminJwt/@SkipPermissions/@SkipTransform. Signature-authenticated.
AiPricingAdminController/admin/ai-pricingAdminAuthGuard + SuperAdminGuardPlatform pricing config.
AiCostAnalyticsController/admin/ai-analyticsAdminAuthGuard + SuperAdminGuardRead-only cost dashboard.

User billing endpoints authenticate with the user JWT (not the admin JWT) and require an active workspace on the user (user.workspaceId, else 400 "No workspace selected"). They carry no PermissionGuard/@RouteName — the only gate is the read-only guard below. The plan catalog (GET /admin/pricing/plans) lives in the separate pricing module.

WorkspaceReadOnlyGuard (global)

Registered as an APP_GUARD in billing.module.ts. On every write (POST/PUT/PATCH/DELETE) it checks the workspace's subscription.read_only_since; if set it throws 402 PAYMENT_REQUIRED. Reads (GET/HEAD/OPTIONS) always pass. @AllowWhenReadOnly() (billing/payment + workspace-deletion) bypasses it. It decodes its own JWT, so guard ordering is irrelevant.

Database tables (MAIN db)

TableEntityHolds
subscriptionsSubscriptionOne per workspace: status, trial/period/grace clocks, read_only_since, Paymob gateway_* ids, seat_count, custom_price/custom_limits.
payment_eventsPaymentEventInvoice + audit ledger. Unique (gateway, gateway_event_id) ⇒ webhook idempotency.
addonsAddOnGlobal seeded add-on catalog.
workspace_addonsWorkspaceAddOnActive recurring add-ons per workspace (unique per workspace+addon).
workspace_consumablesWorkspaceConsumableOne-time pack balances (workflow / AI).
workspace_usage_countersWorkspaceUsageCounterPer-workspace/metric/month counters.
ai_model_pricing · ai_credit_setting · ai_usage_eventAI-run cost catalog + immutable ledger (super-admin).

Events & queues

  • Domain events (EventEmitter2): billing.seat.changedSeatChangeListenerapplySeatChange; usage.recorded (emitted by UsageCounterService) → realtime usage:updated. See Events.
  • Bull queue billing-enforcementSubscriptionEnforcementProcessor: suspend-workspace, unsuspend-workspace. (No renewal job — Paymob auto-debits the native subscription.)
  • Schedulers (@Cron, env-overridable): TrialExpiryScheduler (card-less trials → past_due), GraceExpiryScheduler. There is no renewal scheduler — renewals are auto-debited by Paymob. The full enforcement ladder (read-only-delete, workspace-purge) is in Overview §11.

Multi-tenant safety

Billing tables are MAIN-db and keyed by user_workspace_id, so they are not subject to hyper-tenant RLS pinning — but any metered write you add through UsageModule runs on the caller's identity via WorkspaceIdentityService. See Data isolation before touching cross-tenant paths.

On this page