Pages, plans and data
These contracts let consumers build backend-declared DMS pages without importing private SaaS controllers.
Page extensions
Import from /pages. tenantBillingPage identifies settings.workspace.billing at /settings/workspace/billing. Its components exposes freeAccessBanner, pastDueAlert, planCard, paymentMethod, billingForm, refundButton, and table anchors. TenantBillingPageComponentKey is the union of those keys; tenantBillingPageAnchorPaths maps them to the current nested layout. Use keys instead of copying paths.
RegisterTenantBillingPageExtension(extension: TenantBillingPageExtension): TenantBillingPageExtensionRegistration registers { name: string, components: readonly TenantBillingPageExtensionComponent[] } and returns { unregister(): void }. Registration may precede page loading; call the handle's unregister() during stop().
Each component has key: string, component: Component, and optional order: number (default 0). An anchored component adds side: "before" | "after" and anchorKey: TenantBillingPageComponentKey; an appended component has optional side: "end" and no anchor. These are exported as TenantBillingPageExtensionComponentBase, TenantBillingPageAnchoredExtensionComponent, and TenantBillingPageAppendedExtensionComponent. The plan/payment anchors resolve inside their column's stack, not to an extra grid column.
platformSaasModule: CategoryInfo is a synchronous descriptor, not the canonical registered module object. GetPlatformSaasModule(): Promise<CategoryInfo> resolves that object after runtime attachment and rejects when unavailable. platformWorkspaceDetailPage identifies modules.saas.customers.workspaces.detail, with header, summary and body anchors.
RegisterPlatformWorkspaceDetailPageExtension(extension: PlatformWorkspaceDetailPageExtension): PlatformWorkspaceDetailPageExtensionRegistration accepts { name: string, components: readonly PageExtensionComponent[] } and returns an unregister handle. Use the DMS PageExtensionComponent placement contract for these blocks. These registrations do not grant access: the platform detail remains owner-only, and tenant page components need appropriate metadata/permissions.
/page-definitions exports MODULES_ROOT_DEFINITION: PageInfo, SAAS_MODULE_ID = "saas", and SAAS_MODULE_DEFINITION (full ID, full slug and module registration metadata). These are descriptors, not runtime registration functions. The workspace settings category itself comes from @antelopejs/interface-dms/page.
import { PageController, RegisterPage, workspaceSettingsCategory } from "@antelopejs/interface-dms/page";
@RegisterPage()
export class WorkspaceRegistriesController extends PageController("registries", {
displayName: "Registries",
category: workspaceSettingsCategory,
icon: "i-ph-package",
order: 10,
}) {}
Plan helpers
Import from /plans. These helpers do not charge customers or authorize access.
| Function | Contract |
|---|---|
monthlyPrice(plan: Plan): number | Returns the price as a monthly amount; yearly prices divide by 12 |
isDowngrade(current: Plan | null, target: Plan, seats?: number): boolean | Compares monthly cost, multiplying seat plans by at least one seat; seats default to 1. No current plan returns false; equal cost is not a downgrade |
localizeFeature(feature: Feature, locale: string): Feature | Applies localization to displayName, description, tooltip, and unit, listed by exported FEATURE_LOCALIZED_FIELDS; missing translations fall back to the table locale (en) |
buildTenantPlanCatalog(planModel: PlanModel, featureModel: FeatureModel, plans: Plan[], locale: string): Promise<TenantPlanCatalog> | Reads features, resolves plan inheritance, excludes unknown feature IDs and sorts plans/features by order; model read failures reject |
TenantPlanCatalog has plans: TenantPlanView[] and features: TenantPlanFeature[].
TenantPlanView:_id: string,name: string,price: number,currency: string,interval: PlanInterval(monthoryear),order: number,checkoutAvailable: boolean, andfeatureValues: Record<string, unknown>. Checkout availability reflects a Stripe price reference, not an authorization decision.TenantPlanFeature:featureId: string,displayName: string,tooltip: string | null,unit: string | null,valueType: FeatureValueType(boolean,number, orstring),isDetailRow: boolean, andorder: number.
Keep caller-supplied plan/model scopes authorized. The catalogue builder does not filter an arbitrary plan list to what a user may buy. Write localized feature values using the canonical model's localize("*") convention; see the feature integration guide.
HiddenStringFilter
HiddenStringFilter(): PropertyDecorator is exported from /data-api and /hidden-filter. It registers string comparisons, including is for TableView.routeParamFilters, without a visible @Column. Apply it to the data controller prototype field, such as _instance or userId.
An unregistered filter can be silently dropped by the data API, so declaring a hidden route filter matters for scoping. The decorator itself does not authenticate a user or make a client-supplied tenant ID trustworthy; enforce the scope at the server authorization boundary.
Canonical database surface
/db exports the table/model pairs for plans, features, subscriptions, billing info/state/settings, invoices, credit notes, refunds, legal documents, trial consumptions, segments, user segments, platform notes, plan migrations and Stripe webhook events. /db/models and /db/tables expose those groups separately. For example, PlanModel works with Plan, FeatureModel with Feature, and TenantSubscriptionModel with TenantSubscription; use their shipped TypeScript declarations for persistence fields and methods.
These are registered persistence objects, not copies to redeclare. Imports have registration side effects; prefer type-only imports for types. Direct model writes bypass runtime orchestration and require correct tenant scope. See the database boundary before writing seeds or imports, and recompute derived billing state after writing relevant records.