[data-reveal]{opacity:1!important;transform:none!important}
FAQ
SaaS

FAQ

Answers to the common questions and problems with the SaaS module, and the database fixes an upgrade needs.

A visitor who just registered stays signed out

The frontend server does not allow the finalize endpoint. Set DMS_AUTH_ESTABLISH_ENDPOINTS=/api/saas/register/finalize in its environment; without it the loader answers 403. See Public screens.

Registration answers 409 saas.errors.plan.no_free_plan

The catalogue has no active free plan open to individuals. Publish one, or point defaultPlanSlug at one. See Default plan.

Choosing a plan answers 400 saas.errors.plan.not_synced_with_stripe

The plan is not linked to a Stripe price yet: the sync failed or Stripe keys are placeholders. Check the logs, then save the plan again or wait for the next sync pass. See Plans and Stripe.

Checkout answers 409 saas.errors.plan.checkout_in_progress

A Stripe Checkout session for the workspace is still open. Finish or abandon it; an expired or never-recorded session is released on the next attempt. See Starting a paid plan.

"Manage in Stripe" or Checkout refuses to redirect

The return URL's host is not in allowedRedirectHosts. Loopback hosts are only accepted in development.

The next invoice preview says "unavailable"

Stripe could not price the invoice: placeholder keys, Stripe Tax inactive, an invalid tax location, a usage provider that failed, or a Stripe answer slower than stripe.readTimeoutMs (4 s by default; see Configuration). The reason of the upcoming invoice preview says which.

A member lost a permission their role grants

Plans cap permissions: a member keeps only the permissions both their roles and the workspace's plan grant, and the owner gets every permission of the plan. Add the permission to the plan. See Tenant owner permissions.

A plan migration or webhook is stuck in "reconciliation required"

That is deliberate: the attempt may have partial effects. Establish what happened and that the previous executor stopped before resolving it. See Operations.

Upgrading

To 0.6 (DMS 0.9)

dms-saas 0.6 requires @antelopejs/dms 0.9, @antelopejs/interface-dms 0.7 and the renderer @antelopejs/dms-frontend 0.6. Upgrade those dependencies together before the module; Troubleshooting covers dependency resolution and frontend integration errors. The configuration and the data do not change; stripe.readTimeoutMs and stripe.readMaxNetworkRetries are new and optional.

Tenant billing page anchors

The plan, next invoice, payment method and money-back cards of the tenant billing page share a two-column grid (overview), so planCard, paymentMethod and refundButton are no longer top-level fields of the page. The names in tenantBillingPage.components are unchanged, and RegisterTenantBillingPageExtension maps each one to its position through tenantBillingPageAnchorPaths: a block registered after planCard still lands right under the plan card, in its column. Upgrade @antelopejs/interface-dms-saas together with @antelopejs/dms-saas: an earlier build of the interface anchors on the old top-level fields, which the DMS no longer finds, and the block is then appended at the end of the page. Code that anchors with the DMS's own @RegisterPageExtension passes the path, as in .after(tenantBillingPageAnchorPaths.planCard).

The permission ids of those four cards follow their position (settings.workspace.billing.overview.plan.current.planCard, and so on), so a plan or role granting them needs the new ids.

Rows written by earlier builds

Billing no longer infers state from rows that predate the current fields. Rows imported from the pre-DMS codebase or written by earlier builds must be fixed in the database, with writers stopped, before upgrading:

  • features.displayName and features.tooltip are localized. Rows written by earlier builds hold a plain string (often a $ i18n key) that the localized readers cannot use. Drop the features table and let the declaring modules write it again on startup, then re-enter any feature created by hand.
  • tenant_subscriptions.isComplimentary is required. Only isComplimentary: true makes a subscription complimentary; a missing value is read as not complimentary. Set isComplimentary: true on admin gifts that lack it (typically rows with no stripeCustomerId, no stripeSubscriptionId and a status other than pending_payment), otherwise they are billed and shown as paid plans. Backfill isComplimentary: false on every other row that lacks the field, including workspaces created through paid provisioning before this change, so the stored rows match the table type.
  • invoices.documentType is required. Invoice lists and reads only match documentType: "invoice"; a row without it is neither listed nor readable. Set documentType: "invoice" on every invoice row that lacks it.
  • stripe_webhook_events.result no longer accepts failed. Such attempts may carry partial effects: establish their outcome as described for reconciliation_required above, then set their result to reconciliation_required or delete the record once resolved.
  • revision is required on tenant_subscriptions, segments, user_segments, plan_migrations, stripe_webhook_events, saas_lifecycle_deliveries and saas_operator_actions. Revision-fenced writes no longer adopt a row without one: they refuse it, so the workflow holding that row stalls. Give every row lacking the field (absent or null) an initial revision, any unique string such as a fresh UUID. Segments created through the platform data API by earlier builds are the usual case.
  • tenant_subscriptions.paidUsagePeriods is required. It is the only source of usage coverage: a renewal invoice bills usage only inside a period recorded for its own Stripe subscription, and a missing ledger is no longer read as "bill the whole window". Rows lacking it (or holding null) are typically default-plan, free-plan and pending-checkout subscriptions, and workspaces created through paid provisioning. Set it by kind:
    • a row with a stripeSubscriptionId, not complimentary: [{ stripeSubscriptionId, start, end: null }], where start is the Stripe subscription's start_date (or any earlier date), so renewals keep billing their whole window;
    • every other row (no Stripe subscription, or isComplimentary: true): [].