Skip to main content

Subscriptions, credits and payments tables

Fifteen billing-domain tables (175 columns). Global rules: all authoritative amounts are fixed-point micros integers (*_micros, 1e-6) or *_amount_minor in the settlement currency's smallest unit; REAL columns are display mirrors. Switches like settlement_currency and credits_per_usd (0 = the credit system is off platform-wide) live in settings (notation per the overview).

user_groups — membership tiers

20 columns. The row ug_free (is_default=1) always exists, created by Seed.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)ug_ + 12 hex chars; the default tier is always ug_free
nameTEXTnotier name; lower(trim(name)) unique
descriptionTEXTno''description
featuresTEXT (JSON)no'[]'JSON array of selling points shown on the subscription page
monthly_price_amount_minorINTEGER → BIGINTno0monthly price in minor units of the settlement currency. Legacy columns price_amount_minor/price_usd/price_cny were backfilled once by migration (store.go:720-740)
yearly_price_amount_minorINTEGER → BIGINTno0yearly price
is_defaultINTEGERno01 = default tier for new users (exactly one row is 1)
sort_orderINTEGERno0subscription-page ordering
max_projectsINTEGERno0project cap per member (0 = unlimited)
max_kbsINTEGERno0knowledge-base cap
max_workspacesINTEGERno0※ ALTER-added: cap on owned workspaces
max_storage_mbINTEGERno0non-image upload storage quota in MB (0 = unlimited; images excluded)
credit_allowanceREALno0display mirror of the timed allowance
credit_allowance_microsINTEGER → BIGINTno0authoritative timed credit allowance (fixed-point)
credit_period_secondsINTEGERno0timed-credit refresh window (0 = no timed credits, permanent only)
is_publicINTEGERno1※ ALTER-added: listed on the public subscription page
is_purchasableINTEGERno1whether checkout is open for this tier (can pause temporarily)
permissionsTEXT (JSON)no'{}'normalized group capability/resource policy (RBAC caps; empty object normalizes to the permissive policy, user_group_permissions.go)
created_atINTEGER → BIGINTnonow()creation time
updated_atINTEGER → BIGINTnonow()update time
  • Index: idx_user_groups_name_unique (UNIQUE ON lower(trim(name))).

credit_ledger — authoritative debit ledger

Billing records; never deleted by usage-log cleanup. 11 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)entry id
user_idTEXTnoFK→users(id), CASCADE
group_idTEXTnotier at time of debit (soft reference, no FK — deleting a tier never erases its ledger)
cycle_anchorINTEGER → BIGINTno0cycle anchor (= users.credit_cycle_anchor)
cycle_startINTEGER → BIGINTno0start of this cycle
kindTEXTnotimed_debit (allowance) / permanent_debit (balance) — constants store/credits.go:15-16
amountREAL → DOUBLE PRECISIONnodisplay mirror
amount_microsINTEGER → BIGINTno0authoritative debit (fixed-point)
source_typeTEXTno''source kind (e.g. message turn)
source_idTEXTno''source id (idempotent traceability)
created_atINTEGER → BIGINTnonow()debit time
  • Indexes: idx_credit_ledger_timed(user_id, group_id, cycle_anchor, cycle_start, kind) (window aggregation), idx_credit_ledger_user_time(user_id, created_at) (both created during migration, store.go:574-575).

credit_reservations — pre-flight holds

Funds reserved before generation to prevent overspend. 10 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)reservation id
user_idTEXTnoFK→users(id), CASCADE
amount_microsINTEGER → BIGINTnoheld amount, CHECK (> 0)
actual_microsINTEGER → BIGINTno0settled amount, CHECK (>= 0)
source_typeTEXTno''source kind
source_idTEXTno''source id
statusTEXTno'reserved'state machine reservedsettlingsettledreleased (CHECK)
expires_atINTEGER → BIGINTnodangling-hold reclaim time, CHECK (> 0)
created_atINTEGER → BIGINTnonow()creation time
updated_atINTEGER → BIGINTnonow()update time
  • Unique: UNIQUE(source_type, source_id) — double-holds on one source are blocked at DB level. Index: (user_id, status, expires_at).

credit_adjustment_notifications — adjustment notices

One-time notices created by admin permanent-credit adjustments. 7 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)notice id
user_idTEXTnoFK→users(id), CASCADE
directionTEXTnoadd / remove (CHECK)
amount_microsINTEGER → BIGINTnopositive adjustment, CHECK (> 0)
reasonTEXTno''admin-supplied reason
created_atINTEGER → BIGINTnonow()creation time
claimed_atINTEGER → BIGINTno0set atomically (CAS) when the signed-in user fetches the notice — refreshes and other devices cannot show it twice
  • Index: idx_credit_adjustment_notifications_user_pending(user_id, claimed_at, created_at, id).

quota_ledger — quota windows

Windowed model/global quota holds and finals. 14 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)qr_ + 12 hex chars
user_idTEXTnoFK→users(id), CASCADE
scope_typeTEXTnoCHECK (non-blank). Values: model_chat / model_image / daily_image / daily_token (store/quotas.go:107-110)
model_idTEXTno''set for model-level quotas (soft reference)
group_idTEXTno''tier window attribution (soft reference)
cycle_anchorINTEGER → BIGINTno0cycle anchor
window_startINTEGER → BIGINTnowindow origin, CHECK (> 0)
limit_typeTEXTnocount / cost (CHECK)
reserved_microsINTEGER → BIGINTno0hold
actual_microsINTEGER → BIGINTno0final
statusTEXTno'reserved'reserved / finalized / released (CHECK)
expires_atINTEGER → BIGINTnoCHECK (> window_start)
created_atINTEGER → BIGINTnonow()creation time
updated_atINTEGER → BIGINTnonow()update time
  • Index: idx_quota_ledger_scope(user_id, scope_type, model_id, group_id, cycle_anchor, window_start, status).

billing_usage — per-message billing rows

Immutable billing facts per message/purpose. 12 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)row id
user_idTEXTyesNULLFK→users(id), SET NULL (anonymized on account deletion)
conversation_idTEXTno''snapshot
message_idTEXTno''snapshot
model_idTEXTno''snapshot
purposeTEXTno''same taxonomy as usage_logs.purpose (chat/task.*/image/embedding/verify)
cost_microsINTEGER → BIGINTno0cost (fixed-point), CHECK (>= 0)
images_countINTEGERno0image count
input_tokensINTEGERno0input tokens
output_tokensINTEGERno0output tokens
currencyTEXTno'USD'CHECK (non-blank)
created_atINTEGER → BIGINTnonow()time
  • Indexes: (message_id, purpose, created_at), (user_id, created_at).

credit_packages — permanent-credit top-up SKUs

9 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)package id
nameTEXTnoproduct name
descriptionTEXTno''description
creditsREALnoface credit amount (PG keeps REAL/float4; money math converts via micros)
price_amount_minorINTEGER → BIGINTno0price in minor units of the settlement currency
enabledINTEGERno1purchasable flag
sort_orderINTEGERno0ordering
created_atINTEGER → BIGINTnonow()creation time
updated_atINTEGER → BIGINTnonow()update time
  • Index: idx_credit_packages_order(sort_order, name). A special legacy permanent-credit row is migrated by MigrateLegacyCreditPackage.

model_group_quotas — model × tier quotas

5 columns. Semantics: a model with no rows here is open to all tiers; once any row exists, only the listed tiers may use it; limit_value=0 = open but metered.

ColumnTypeNullableDefaultNotes
model_idTEXTno (PK part)FK→models(id), CASCADE
group_idTEXTno (PK part)FK→user_groups(id), CASCADE
period_secondsINTEGERno604800window length (default 7 days), CHECK (> 0)
limit_typeTEXTno'count'cost (in the model's currency) / count (calls), CHECK
limit_valueREALno0window cap (0 = open); PG keeps REAL
  • PK: composite (model_id, group_id). Index: idx_mgq_group(group_id).

redeem_codes — redeem codes

14 columns. Code format XXXX-XXXX-XXXX in Crockford base32 minus ambiguous characters (ids.go:32-38).

ColumnTypeNullableDefaultNotes
idTEXTno (PK)rc_ + 12 hex chars
codeTEXTnocode text, UNIQUE database-wide
kindTEXTno'group'group (grants a tier) / credits (grants permanent credits). For credit codes group_id is only an FK-satisfying placeholder and never applies
group_idTEXTnoFK→user_groups(id), CASCADE
duration_daysINTEGERno30membership duration (0 = permanent), CHECK (>= 0)
creditsREALno0granted credits when kind=credits, CHECK (>= 0)
max_usesINTEGERno1redemption capacity, CHECK (> 0)
used_countINTEGERno0uses so far, CHECK (>= 0 AND <= max_uses)
expires_atINTEGER → BIGINTno0deadline for redeeming the code itself (0 = no deadline)
enabledINTEGERno10 = revoked without deleting the row (keeps audit history)
noteTEXTno''admin note
batch_nameTEXTno''batch label
created_byTEXTno''creating admin id (soft reference)
created_atINTEGER → BIGINTnonow()creation time
  • Indexes: idx_redeem_codes_code(code), idx_redeem_codes_batch(batch_name).

redeem_redemptions — redemption audit

8 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)redemption id
code_idTEXTnoFK→redeem_codes(id), CASCADE
user_idTEXTnoFK→users(id), CASCADE
group_idTEXTnoFK→user_groups(id), CASCADE (granted tier)
previous_group_idTEXTno''tier before redemption (for expiry restore)
creditsREALno0granted credits (audit)
granted_atINTEGER → BIGINTnoeffective time (no default, written explicitly)
expires_atINTEGER → BIGINTnomembership expiry
  • Unique: UNIQUE(code_id, user_id) — one user cannot double-redeem the same code (per-user cap on multi-use codes). Indexes: user_id, code_id.

payment_channels — payment channels

9 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)paych_ + 12 hex chars
nameTEXTnochannel name; lower(trim(name)) unique
providerTEXTnostripe / epay / waffo (constants payment/payment.go:58-60)
environmentTEXTno'live'live / test (store/payments.go:25-26; legacy rows backfilled once)
configTEXT (JSON)no'{}'provider-specific JSON containing credentials in plaintext — masked only by the API
enabledINTEGERno1enabled flag
sort_orderINTEGERno0ordering
created_atINTEGER → BIGINTnonow()creation time
updated_atINTEGER → BIGINTnonow()update time
  • Indexes: name UNIQUE + idx_payment_channels_order(sort_order, name). Before deletion, its payment_methods must be removed (RESTRICT, below) and no orders may be pending (app-level checks).

payment_methods — user-selectable payment methods

Options a channel exposes to users. 10 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)paym_ + 12 hex chars
channel_idTEXTnoFK→payment_channels(id), ON DELETE RESTRICT — methods must be deleted before the channel
nameTEXTnodisplay name; (channel_id, lower(trim(name))) unique
typeTEXTnomethod type (free string, e.g. alipay, card — EPay maps it via params["type"])
iconTEXTno''icon
configTEXT (JSON)no'{}'method-level config
enabledINTEGERno1selectable flag
sort_orderINTEGERno0ordering
created_atINTEGER → BIGINTnonow()creation time
updated_atINTEGER → BIGINTnonow()update time

payment_orders — payment orders

Immutable commercial snapshot + mutable processing state: provider, channel/method, product, and amounts are copied at creation so later admin catalog edits never rewrite history. 38 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)order id
user_idTEXTyesNULLFK→users(id), SET NULL (order survives account deletion)
user_emailTEXTnobuyer email snapshot
providerTEXTnogateway kind snapshot
environmentTEXTno'live'live / test (test orders are admin-only)
channel_idTEXTnosnapshot (no FK — channels can be deleted while orders stand)
channel_nameTEXTnosnapshot
method_idTEXTnosnapshot
method_nameTEXTnosnapshot
method_typeTEXTnosnapshot
method_configTEXT (JSON)no'{}'method config as of order creation
product_typeTEXTnocredit_package / user_group (store/payments.go:19-20)
product_idTEXTnoproduct id snapshot
product_nameTEXTnoproduct name snapshot
amount_minorINTEGER → BIGINTnocatalog amount (tax-exclusive)
paid_amount_minorINTEGER → BIGINTno0provider-settled total actually paid
tax_amount_minorINTEGER → BIGINTno0tax added at checkout (VAT/GST processors like Waffo Pancake)
currencyTEXTnocatalog currency
provider_amount_minorINTEGER → BIGINTno0provider-side settlement amount (legacy rows backfilled to amount_minor, store.go:544-547)
provider_currencyTEXTno''settlement currency (backfilled to currency)
conversion_rateTEXTno''conversion rate kept as a string for precision
creditsREAL → DOUBLE PRECISIONno0entitlement: credits granted (when product = credit_package)
user_group_idTEXTno''entitlement: target tier
billing_cycleTEXTno''monthly / yearly (store/payments.go:22-23)
provider_order_idTEXTno''gateway order reference
provider_payment_idTEXTno''gateway payment reference (e.g. Stripe PaymentIntent)
checkout_session_idTEXTno''Stripe Checkout session id
checkout_urlTEXTno''hosted checkout URL
checkout_expires_atINTEGER → BIGINTno0checkout link expiry
last_reconciled_atINTEGER → BIGINTno0last reconciliation attempt
reconcile_errorTEXTno''last reconciliation failure
statusTEXTno'pending'state machine: pending / processing / fulfilled / failed / cancelled (British spelling) / expired (store/payments.go:28-33)
failure_codeTEXTno''failure code (includes admin_manual_close for manual closure)
failure_messageTEXTno''failure detail
paid_atINTEGER → BIGINTno0payment confirmation time
fulfilled_atINTEGER → BIGINTno0entitlement delivery completion
created_atINTEGER → BIGINTnonow()creation time
updated_atINTEGER → BIGINTnonow()update time
  • Indexes: (user_id, created_at DESC, id), (channel_id, status, created_at), (status, created_at); two partial unique indexes (created during migration): (provider, channel_id, provider_order_id) WHERE provider_order_id<>'' and (provider, channel_id, provider_payment_id) WHERE provider_payment_id<>'' (store.go:576).

payment_order_attempts — checkout attempts

One commercial order may have several checkout attempts. 9 columns.

ColumnTypeNullableDefaultNotes
merchant_order_idTEXTno (PK)merchant order number sent to the gateway
order_idTEXTnoFK→payment_orders(id), CASCADE
providerTEXTnogateway kind
channel_idTEXTnochannel snapshot
provider_order_idTEXTno''gateway-returned reference
statusTEXTno'issued'issued / paid (store/payments.go:35-36)
paid_atINTEGER → BIGINTno0payment time of this attempt
created_atINTEGER → BIGINTnonow()creation time
updated_atINTEGER → BIGINTnonow()update time
  • Indexes: (order_id, created_at, merchant_order_id); partial unique (provider, channel_id, provider_order_id) WHERE provider_order_id<>''.
  • Notes: EPay resume-checkout reuses the outstanding merchant_order_id — the integration never issues a replacement without trusted proof the gateway reference ended; ambiguous legacy orders with multiple references fail closed.

payment_events — verified provider notifications

8 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)local event id
providerTEXTnogateway kind
channel_idTEXTnochannel
event_idTEXTnoprovider event id
order_idTEXTnoFK→payment_orders(id), CASCADE
event_typeTEXTno''raw provider event type (e.g. Stripe checkout.session.completed; free text)
created_atINTEGER → BIGINTnonow()receipt time
processed_atINTEGER → BIGINTno0fulfillment completion (0 = unprocessed)
  • Unique: UNIQUE(provider, channel_id, event_id) — the first idempotency barrier; fulfillment additionally locks and re-checks the order, so two different success event ids can never grant value twice. Index: (order_id, created_at, id).