Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesSubscriptions PlansSubscriptions & Plans — Data Model

Subscriptions & Plans — Data Model

Schema in libs/db/src/lib/schema/payments.ts.

plans (payments.ts:31)

ColumnTypeNotes
iduuid PK
organization_iduuid FKMulti-tenant scope.
name, descriptionvarchar(255), text
typeplan_type enum`subscription
program_iduuid FK → programs.idRequired when type='course'. Validated in service layer (no DB-level constraint).
price_in_centsintegerCents (0 = free).
currencyvarchar(3)ISO-4217. Resolved from organizations.currency at service layer.
intervalplan_interval enumRequired when type='subscription'. `weekly
class_creditsintegerRequired when type='class_pack'. Optional on subscription (null = unlimited).
max_bookings_per_day, max_bookings_per_weekintegerBooking caps consumed by bookings/.
allow_overlapping_bookingsboolDefault false.
provider_price_id, provider_product_idvarchar(255)Currently unused; for future native-recurring providers.
is_activeboolSoft-delete flag.
deleted_attimestamptzSoft delete marker (independent from is_active).

Index: plans_org_id_idx (organization_id).

subscriptions (payments.ts:117)

ColumnTypeNotes
iduuid PK
membership_iduuid FK → memberships.idThe owner. Multi-org scope is via membership.
plan_iduuid FK → plans.id
provider_subscription_idvarchar(255)Provider-native subscription handle when available. Cardcom returns null (we drive recurring ourselves).
statussubscription_status enum`pending
current_period_starttimestamptz
current_period_endtimestamptz
remaining_creditsintegerDecremented by bookings. Refilled per period. Null on unlimited subs.
debt_amount_in_centsintegerDefault 0. Incremented on each renewal failure once attempts ≥ 3.
debt_sincetimestamptzStamped on entering debt.
failed_charge_attemptsintegerResets to 0 on success.
next_charge_datetimestamptzRenewal cron filter. NULL when in debt (no auto retry).
billing_dayintegerDay-of-month for monthly subs (currently informational; cron uses next_charge_date).
provider_direct_debit_idvarchar(255)Reserved.
payment_method_iduuid FK → member_payment_methods.idStored card.
paused_attimestamptzSet by freezeSubscription; cleared by resumeSubscription; period extended by now − paused_at.
cancelled_attimestamptzTerminal.
cancel_at_period_endboolFlag on an active row. Cron flips status when deadline passes.
cancellation_reasontextFree-form.
cancellation_requested_attimestamptz
cancellation_requested_byuuid FK → users.idActor record for audit.
deleted_attimestamptzReserved soft-delete.

Indexes:

  • subscriptions_membership_id_idx (membership_id)
  • subscriptions_status_next_charge_idx (status, next_charge_date) — renewal cron sweep.
  • subscriptions_current_period_end_idx (current_period_end) — cancellation cron.
  • subscriptions_cancel_at_period_end_idx (cancel_at_period_end, current_period_end) — cancellation cron filter.
  • subscriptions_status_created_at_idx (status, created_at) WHERE deleted_at IS NULL — MRR analytics.

cancellation_requests (payments.ts:405)

ColumnTypeNotes
iduuid PK
organization_iduuid FK
subscription_iduuid FK
requested_by_user_iduuid FK → users.id
reasontext
refund_requestedbool
refund_amount_in_centsintegerOptional override; null = full refund of most-recent charge.
statuscancellation_request_status enum`pending
resolved_attimestamptz
resolved_by_user_iduuid FK
resolver_notetext
refund_task_iduuid FK → tasks.idSet on approval when manual capability.

Indexes:

  • cancellation_requests_org_status_idx (organization_id, status) — owner inbox.
  • cancellation_requests_subscription_idx (subscription_id) — duplicate-pending guard.

Relationships

organizations 1───* plans organizations 1───* cancellation_requests memberships 1───* subscriptions plans 1───* subscriptions plans 0───1 programs (only when type='course') subscriptions 1───* cancellation_requests subscriptions ?───1 member_payment_methods (subscriptions.payment_method_id) users 1───* cancellation_requests (requestedBy / resolvedBy roles) tasks 1───1 cancellation_requests (via refund_task_id)

Multi-org isolation

  • plans.organization_id and cancellation_requests.organization_id are direct.
  • subscriptions has no organization_id; isolation is via subscriptions.membership_id → memberships.organization_id. Every service query joins memberships and filters (subscriptions.service.ts:225, :505).
  • Bookings / payments cross-check via the same join.

Money columns

All amounts on this side are cents (price_in_cents, debt_amount_in_cents, refund_amount_in_cents).

Compare with platform_billing_*.amount which is major units — see platform-billing/data-model.md.

Lifecycle table

StateBooking allowed?Renewal cron eligible?Bookings consume credits?
pendingNoNo (excluded by selector)No
activeYesYesYes
past_dueYes (configurable per org; currently allowed)Yes (retry attempts < 3)Yes
debtNo (downstream services reject membership.paymentStatus='debt')No (selector includes only `activepast_due`)
pausedNoNoNo
cancelledNoNoNo