Subscription Management
Stripe subscriptions, plan changes, recurring credits, webhook health, and launch checks.
Last updated May 30, 2026
This guide is for site owners and delivery teams. It explains what the current ZShip subscription system supports, what must be configured, how to validate it before launch, and where to look when something behaves unexpectedly.
Subscription management spans multiple shared services and layers:
backend/node3-pay-service: checkout, Stripe subscription mutations, webhook processing, and subscription change history.backend/node1-auth-service: user subscription read model, subscription credits, and API key entitlement resolution.packages/nuxt-common-layer: user dashboard subscription page, Change Plan modal, cancel and resume actions.packages/nuxt-admin-layer: admin plans, pay channels, webhook audit, webhook event health, and user subscription management.backend/zship-provider1-service: optional subscription-based provider/model access control.
Current capabilities
Stripe subscriptions
Stripe is the provider with complete subscription management support. The system supports:
- Initial subscription checkout.
- Renewal invoices refreshing the billing period and subscription credits.
- Immediate upgrades with Stripe proration invoices.
- Pending payment or SCA flows for immediate upgrades.
- Automatic cleanup when a pending update expires or fails.
- Downgrades and interval changes scheduled for the end of the current period.
- Stripe subscription schedules for end-of-period plan changes.
- Cancel at period end.
- Resume by clearing
cancel_at_period_end. - Synchronization of statuses such as
active,past_due,unpaid,paused,canceled,incomplete, andincomplete_expiredinto the Node1 read model.
Creem and mock subscriptions
Creem and mock flows can be used for payments, subscription state, or credit tests, but they do not support Stripe subscription management operations.
The UI follows that boundary:
- Change Plan / Cancel / Resume are shown only when the provider is
stripeandprovider_subscription_idexists. - Creem and mock subscriptions do not show Stripe-only actions.
- Legacy rows with an empty provider but a Stripe subscription id are treated as legacy Stripe subscriptions.
Data ownership
Node3 Pay is the payment operation source
node3-pay-service talks to Stripe and owns payment mutations:
- Create Stripe Checkout Sessions.
- Update Stripe subscriptions.
- Create or update Stripe subscription schedules.
- Verify Stripe webhook signatures.
- Write
t_webhook_audit. - Write
t_subscription_change. - Sync the latest subscription state to Node1.
Important tables:
t_price_config: plans, Stripe price ids, and subscription credit settings.t_price_config_stripe_channel: Stripe price overrides for multiple channels.t_subscription_change: upgrade, downgrade, cancel, resume, pending payment, and related history.t_webhook_audit: raw webhook event audit.
Node1 Auth is the read model and entitlement source
node1-auth-service stores the state read by users, AI providers, API keys, and the credit system.
Important tables:
t_user_subscription: current subscription, pending plan, cancel-at-period-end state, credit cadence, and related fields.t_credit_account: permanent credits and subscription credits.t_subscription_credit_cycle: monthly subscription credit cycle records.
Important behavior:
/credits/balancereturns the current balance and subscription detail./internal/credits/subscription-syncreceives lifecycle syncs from Node3 and does not grant credits./credits/subscription-renewrefreshes the period and credits after a real renewal invoice./keys/resolveexposes subscriber entitlements to Provider Service only for valid subscriptions.
Plan configuration
Create subscription plans in Admin Price Config.
Common required fields:
| Field | Purpose |
|---|---|
app_key |
Tenant project key |
price_type |
Stable plan id, for example pro_monthly or pro_yearly |
is_subscription |
Must be enabled for subscription plans |
stripe_price_id |
Stripe Price ID, unless a channel override is used |
amount / currency / interval |
Used for display and plan-change classification |
tier |
Entitlement tier, for example basic, pro, or team |
credits |
Compatibility field and fallback value |
credit_cadence |
billing_period or monthly |
credit_amount |
Credits granted each subscription credit cycle |
credit_policy |
reset or accumulate |
credit_anchor |
Usually keep the default subscription_start |
Subscription credit rules
credit_cadence = billing_period:
- Grants subscription credits once per real billing renewal.
- Fits monthly plans that grant monthly credits, or annual plans that grant the full annual amount at renewal.
credit_cadence = monthly:
- Stripe still controls the real billing period.
- Node1 grants subscription credits monthly using
next_credit_reset_at. - Fits annual plans that grant credits monthly, such as
mock_annual_monthly_credits.
credit_policy = reset:
- Each grant resets subscription credits to
credit_amount.
credit_policy = accumulate:
- Each grant adds
credit_amountto the current subscription credit balance.
Lifecycle
Initial purchase
- The user selects a subscription plan on Pricing.
- The frontend calls local
/api/pay/checkout. - Node3 creates a Stripe Checkout Session and stores
email,app_key,price_type,tier, andcreditsin metadata. - Stripe sends
checkout.session.completed. - Node3 writes the payment record and calls Node1 to activate the subscription.
- Node1 writes
t_user_subscriptionand grants the initial subscription credits.
Renewal
- Stripe sends
invoice.paid. - Node3 processes only real renewal invoices:
- Skip
subscription_create; checkout handles the initial grant. - Skip
subscription_update; upgrade proration invoices must not grant renewal credits. - Process
subscription_cycleand keep compatibility with legacysubscription.
- Skip
- Before renewal, Node3 resolves the latest plan from the invoice or the current Stripe subscription price.
- If a scheduled plan change has taken effect in Stripe before
customer.subscription.updatedarrives,invoice.paidcan still sync Node1 first. - Node1 refreshes subscription credits using
credit_cadenceandcredit_policy.
Immediate upgrade
Plan changes to a higher amount are treated as immediate upgrades:
- Node3 calls Stripe
subscriptions.update. - It uses
proration_behavior: always_invoice. - It uses
payment_behavior: pending_if_incomplete. - If Stripe succeeds immediately, Node3 syncs the new plan to Node1.
- If payment or SCA is required, Stripe sets
subscription.pending_update, and Node3 records the pending plan. - The UI shows a Complete Payment action.
- Successful payment is handled through
customer.subscription.pending_update_applied. - Expired or failed pending updates are handled through
customer.subscription.pending_update_expired.
When a pending update or pending plan already exists, further plan-change requests return a controlled pending-change response instead of falling into a Stripe pending-update conflict.
End-of-period downgrade or interval change
Lower amount changes, or monthly/yearly interval changes, are scheduled for the period end:
- Node3 creates or updates a Stripe subscription schedule from the current subscription.
- The current phase keeps the current price until
current_period_end. - The next phase uses the target price.
- The schedule uses
end_behavior: release, and the target phase usesiterations: 1, so Stripe releases the schedule after the target phase while the subscription continues. - Node1 stores
pending_plan_id,pending_effective_at, andsubscription_schedule_id. - At period end,
customer.subscription.updatedor renewalinvoice.paidconfirms the target plan and clears pending state.
Cancel and resume
Cancellation is not immediate deletion:
- Cancel sets
cancel_at_period_end = truein Stripe. - Node1 keeps the current plan and shows that it will cancel at period end.
- Resume clears
cancel_at_period_end. - When the subscription really ends, Stripe sends
customer.subscription.deleted; Node1 marks it canceled and clears subscription credits.
Stripe webhook setup
The webhook URL is usually:
https://<your-node3-pay-domain>/stripe/webhook
Admin Pay Channels shows a copyable webhook URL and suggested events for the current project.
Required Stripe events
| Event | Purpose |
|---|---|
checkout.session.completed |
One-time payments and initial subscription activation |
checkout.session.async_payment_succeeded |
Delayed payment success handling |
invoice.paid |
Subscription renewals and billing-period credit refresh |
customer.subscription.created |
New subscription state sync and debugging |
customer.subscription.updated |
Plan changes, cancel marker, and status sync |
customer.subscription.pending_update_applied |
Pending upgrade payment succeeded |
customer.subscription.pending_update_expired |
Pending upgrade expired cleanup |
customer.subscription.deleted |
Subscription ended |
charge.refunded |
Credit and commission clawback |
Recommended Stripe events
| Event | Purpose |
|---|---|
invoice.payment_failed |
Failed renewal alerts |
invoice.payment_action_required |
SCA or manual payment action required |
invoice.finalization_failed |
Invoice finalization failures |
customer.subscription.paused |
Paused subscription status sync |
customer.subscription.resumed |
Resumed subscription status sync |
customer.subscription.trial_will_end |
Trial ending reminders |
checkout.session.async_payment_failed |
Delayed payment failure alerts |
payment_intent.payment_failed |
Payment failure audit |
billing_portal.session.created |
Billing Portal launch audit |
Webhook health
Admin /payments includes:
- Webhook audit log modal.
- Webhook key event health modal.
- Copyable event names.
Health shows whether each key event has ever been received. On a new production site, some events may be missing until they naturally occur, but Stripe Dashboard must still subscribe to the required events.
Admin operations
Price Config
Site owners should configure subscription plans first:
- Open Admin -> Payments -> Price Config.
- Create or edit a subscription plan.
- Fill
tier,credits,credit_cadence,credit_amount, andcredit_policy. - Fill the Stripe Price ID, or configure a channel override.
- Save; Node3 syncs plan settings to Node1 project settings for subscription credit logic.
User subscription management
Admin -> Users -> Manage Subscription shows:
- Current plan, tier, and status.
- Next billing time and time remaining.
- Cancel-at-period-end notice.
- Pending plan change notice.
- Subscription change history.
Only Stripe subscriptions can be changed, canceled, or resumed here. Creem and mock subscriptions show a boundary notice and do not expose Stripe-only actions.
User Change Plan
On the user dashboard Subscription page:
- Change Plan is a button.
- Clicking it opens a modal for target plan selection.
- Plans are not listed directly on the page.
- Plan card action buttons are bottom-aligned.
- If an upgrade requires payment, the UI shows Complete Payment.
AI Provider subscription model access
When zship-provider1-service is enabled, Provider or model access can be restricted by subscription plan:
- Admin -> Providers -> Subscription Plan Model Access.
- Configure allowlists by
app_keyandplan_id. - Both public model IDs and provider-facing model IDs after
model_selectorare checked. - Model list requests pass
subscription_plan_idonly for active subscriptions. - Generation uses the active subscription resolved from the API key as the final authority.
This prevents users from seeing a model that generation later rejects, and prevents model-selector rewrites from bypassing subscription model restrictions.
Mock validation
Mock flows are useful for local or internal validation of subscription credit logic. They do not represent Stripe-manageable subscriptions.
Common scenario:
mock_annual_monthly_credits: annual subscription with monthly subscription credit grants.- Use it to validate
credit_cadence = monthly,next_credit_reset_at, and monthly cycle jobs.
Boundaries:
- Mock provider does not support Stripe plan changes.
- Mock provider does not support Stripe cancellation.
- The UI hides Stripe-only actions for mock rows.
Launch checklist
Migrations
Before deployment, apply these migrations:
backend/node1-auth-service/migrations/0023_subscription_credit_cadence.sqlbackend/node1-auth-service/migrations/0024_subscription_change_state.sqlbackend/node3-pay-service/migrations/0012_subscription_credit_settings.sqlbackend/node3-pay-service/migrations/0013_subscription_management.sql- If Provider subscription model access is enabled:
backend/zship-provider1-service/migrations/0011_subscription_model_access.sql
Environment and bindings
Confirm:
node3-pay-servicecan reach Node1 throughNODE1_AUTHservice binding orNODE1_AUTH_URL.ZSHIP_KEYmatches across internal services.- Stripe Pay Channel has the correct secret key and webhook secret.
- Frontend Pages API URLs point to the correct pay/auth/provider environment.
- Admin and user site use the same
app_key.
Stripe test mode validation
Before real charges, run at least one Stripe test mode pass:
- Register or log in as a user.
- Create an initial subscription.
- Confirm
checkout.session.completedarrives and Node1 has an active subscription. - Trigger renewal
invoice.paidand confirm credits follow the current plan. - Upgrade immediately and confirm the proration invoice does not grant renewal credits.
- Simulate payment action required and confirm Complete Payment plus pending state.
- Let or simulate pending update expiration and confirm pending state clears.
- Downgrade or switch interval and confirm pending plan is shown for period end.
- At period end, confirm the new plan and target credit settings are active.
- Cancel and confirm
cancel_at_period_end. - Resume and confirm the cancel marker disappears.
- End or delete the subscription and confirm Node1 marks it canceled and clears subscription credits.
Common issues
Only Stripe subscription changes are supported
The subscription is not a Stripe provider subscription, or it has no Stripe subscription id.
What to check:
- For mock or Creem, this is expected; Stripe plan changes cannot be performed there.
- For legacy Stripe rows, check
t_user_subscription.provider_subscription_id. - Rows with an empty provider but a Stripe subscription id are treated as legacy Stripe subscriptions.
Credits increased after an immediate upgrade
Check the billing_reason on invoice.paid.
Current renewal logic processes only:
subscription_cycle- legacy-compatible
subscription
It skips:
subscription_createsubscription_update
If credits still look wrong, check whether an older deployment is still processing webhooks.
Downgrade took effect but credits still use the old plan
The current logic syncs Stripe's current price before renewal. Check:
- Whether the Stripe subscription item price is already the target price.
- Whether
t_price_configor channel override can map that Stripe price id to the target plan. - Whether
customer.subscription.updatedorinvoice.paidreached Node3. - Whether Node1
t_user_subscription.credit_amountchanged to the target plan.
Delivery guidance
The code paths have static verification, but Stripe is an external state machine. Run Stripe test mode before production billing.
Recommended site-owner handoff wording:
- There are no known blocking issues at the code level.
- Stripe subscription management supports the main lifecycle.
- Site owners must configure webhooks and complete test mode validation before real charges.
- Creem and mock do not support Stripe subscription management actions; that is a product boundary, not an error.
Include these handoff details:
- Stripe webhook URL.
- Required event list.
- Current
app_key. price_typeto Stripe price id mapping.- Test account and test mode validation notes.
