Docs/Integrations
Integrations

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, and incomplete_expired into 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 stripe and provider_subscription_id exists.
  • 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/balance returns the current balance and subscription detail.
  • /internal/credits/subscription-sync receives lifecycle syncs from Node3 and does not grant credits.
  • /credits/subscription-renew refreshes the period and credits after a real renewal invoice.
  • /keys/resolve exposes 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_amount to the current subscription credit balance.

Lifecycle

Initial purchase

  1. The user selects a subscription plan on Pricing.
  2. The frontend calls local /api/pay/checkout.
  3. Node3 creates a Stripe Checkout Session and stores email, app_key, price_type, tier, and credits in metadata.
  4. Stripe sends checkout.session.completed.
  5. Node3 writes the payment record and calls Node1 to activate the subscription.
  6. Node1 writes t_user_subscription and grants the initial subscription credits.

Renewal

  1. Stripe sends invoice.paid.
  2. 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_cycle and keep compatibility with legacy subscription.
  3. Before renewal, Node3 resolves the latest plan from the invoice or the current Stripe subscription price.
  4. If a scheduled plan change has taken effect in Stripe before customer.subscription.updated arrives, invoice.paid can still sync Node1 first.
  5. Node1 refreshes subscription credits using credit_cadence and credit_policy.

Immediate upgrade

Plan changes to a higher amount are treated as immediate upgrades:

  1. Node3 calls Stripe subscriptions.update.
  2. It uses proration_behavior: always_invoice.
  3. It uses payment_behavior: pending_if_incomplete.
  4. If Stripe succeeds immediately, Node3 syncs the new plan to Node1.
  5. If payment or SCA is required, Stripe sets subscription.pending_update, and Node3 records the pending plan.
  6. The UI shows a Complete Payment action.
  7. Successful payment is handled through customer.subscription.pending_update_applied.
  8. 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:

  1. Node3 creates or updates a Stripe subscription schedule from the current subscription.
  2. The current phase keeps the current price until current_period_end.
  3. The next phase uses the target price.
  4. The schedule uses end_behavior: release, and the target phase uses iterations: 1, so Stripe releases the schedule after the target phase while the subscription continues.
  5. Node1 stores pending_plan_id, pending_effective_at, and subscription_schedule_id.
  6. At period end, customer.subscription.updated or renewal invoice.paid confirms the target plan and clears pending state.

Cancel and resume

Cancellation is not immediate deletion:

  • Cancel sets cancel_at_period_end = true in 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
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:

  1. Open Admin -> Payments -> Price Config.
  2. Create or edit a subscription plan.
  3. Fill tier, credits, credit_cadence, credit_amount, and credit_policy.
  4. Fill the Stripe Price ID, or configure a channel override.
  5. 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_key and plan_id.
  • Both public model IDs and provider-facing model IDs after model_selector are checked.
  • Model list requests pass subscription_plan_id only 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.sql
  • backend/node1-auth-service/migrations/0024_subscription_change_state.sql
  • backend/node3-pay-service/migrations/0012_subscription_credit_settings.sql
  • backend/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-service can reach Node1 through NODE1_AUTH service binding or NODE1_AUTH_URL.
  • ZSHIP_KEY matches 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:

  1. Register or log in as a user.
  2. Create an initial subscription.
  3. Confirm checkout.session.completed arrives and Node1 has an active subscription.
  4. Trigger renewal invoice.paid and confirm credits follow the current plan.
  5. Upgrade immediately and confirm the proration invoice does not grant renewal credits.
  6. Simulate payment action required and confirm Complete Payment plus pending state.
  7. Let or simulate pending update expiration and confirm pending state clears.
  8. Downgrade or switch interval and confirm pending plan is shown for period end.
  9. At period end, confirm the new plan and target credit settings are active.
  10. Cancel and confirm cancel_at_period_end.
  11. Resume and confirm the cancel marker disappears.
  12. 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_create
  • subscription_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_config or channel override can map that Stripe price id to the target plan.
  • Whether customer.subscription.updated or invoice.paid reached Node3.
  • Whether Node1 t_user_subscription.credit_amount changed 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_type to Stripe price id mapping.
  • Test account and test mode validation notes.