Membership & Recurring Billing
Plans, subscriptions and payments modelled on the platform's recurring_plan, membership and payment base types. Worked from a real 39-schema gym application.
The Project
app_m71-gym3 (namespace ironworks_three), in ~/supero-test/e2e-tests/apps/app_m71-gym3. It carries 39 schemas — fifteen of its own, the rest imported by the platform services it uses. Every entity named below is read out of that bundle.Architecture & Design
{"name": "membership_plan",
"namespace": "ironworks_three",
"schema_type": "object",
"parent_type": "tenant",
"extends": "recurring_plan:base_plan",
"attributes": [
{"name": "plan_name", "type": "string", "mandatory": True},
{"name": "category", "type": "string",
"values": ["Basic", "Premium", "VIP"]},
{"name": "billing_interval", "type": "string", "default": "month"},
{"name": "benefits", "type": "string"},
{"name": "is_active", "type": "boolean", "default": True}]}⚠️ Where extends has to live
extends must sit inside schema_content. At the wrapper level it is silently stripped and ignored — the schema saves, nothing errors, and you get an ordinary object type with none of the service behaviour. The canonical field list key is attributes, and entries are {name, type, mandatory}. It is not JSON Schema — there is no properties map. fields is accepted as a synonym for attributes and required for mandatory, but neither is what you read back: a body sent with fields is stored under attributes, while required is left exactly as you wrote it next to the mandatory fields inherited from the base type. Author the canonical spelling and you avoid reconciling two forms in one response — see Schema Reference.| Your type | Extends | What the base type brings |
|---|---|---|
membership_plan | recurring_plan:base_plan | status (inactive, active, paused, cancelled, expired), billing_interval, billing_amount, currency, current_period_start, current_period_end, next_billing_at, external_billing_id |
member_subscription | membership:base_membership | status, role, joined_at, suspended_at, expires_at, suspension_reason, expulsion_reason, transfer_target_uuid |
member_profile | customer:base_customer | Customer identity, parented to user_account rather than to the tenant |
payment | payment:base_payment | status (pending, authorized, captured, refunded, voided, failed), captured_amount, refunded_amount, external_payment_id |
order, order_item | order:base_order, order:base_order_item | Totals, item_count, source_cart_uuid, and the pending-to-fulfilled path |
cart, cart_item | cart:base_cart, cart:base_cart_item | Line items held before checkout |
waiver, waiver_signature | document_signature:base_document and :base_signature | required_signatures, completed_signatures, signature status |
class extending service:base_service and class_booking extending booking:base_booking — the calendar bolted onto the membership — plus trainer, which extends nothing because it is just a record.The Flow
- 1
Publish the plans
Create
membership_planrecords.billing_amount,currencyandbilling_intervalcome fromrecurring_plan:base_plan;categoryandbenefitsare yours.is_activecontrols whether the plan is offered. - 2
A member joins
A
member_profileis created — it extendscustomer:base_customer, so it is parented to a user account, not to the tenant directly. Then amember_subscriptionreferencing the chosen plan. - 3
Take the money
The gym imports
stripe_checkout. Its credentials —stripe_api_keyandstripe_webhook_secret— live in the keystore at/api/v1/keystore/platform-secretsand read back masked, never in your code. - 4
Record the payment
A
paymentrecord carries the result.external_payment_idandexternal_charge_idare what tie it back to the processor. - 5
Renewal comes round
This step is yours to build.
next_billing_atis a field on the plan, and a workflow you define reads it, charges, and advances the period. Nothing charges on its own. - 6
Suspend, cancel or expire
Every transition goes through one endpoint. An illegal move returns
409 INVALID_STATE_TRANSITIONrather than quietly writing a bad status.
POST /api/v1/services/execute
{"service_id": "membership",
"operation": "suspend",
"input": {"extending_schema": "ironworks_three:member_subscription",
"uuid": "..."},
"domain": "m71-corp",
"project_uuid": "..."}What You Get Without Building It
- •The lifecycle fields and state machines above, on every type that extends a base type — including the 409 on an illegal transition.
- •Multi-tenancy. Records address as
fq_name, a list of[domain, project, tenant, record], so one deployment serves many customers with no tenancy code of your own. - •A fixed role set out of the box. The eight you assign are
domain_admin,domain_user,project_admin,project_user,developer,tenant_admin,tenant_userandviewer;platform_adminandplatform_userare Supero-operator roles you will not hand out. - •Credential handling. Stripe, SendGrid and S3 keys are held in the keystore and masked on read; the gym bundle declares exactly which keys each service needs.
- •A generated application on top:
schemas.py,ui_schemas.py,ui/app.js,setup.pyandrun.sh, which is the whole bundle you can run and keep.
💡 Read the bundle, not just the docs
app_m71-gym3/schemas.py too, marked "schema_source": "service" — so you can read the exact field list you inherit before committing to extend anything.How to Implement in Supero
Model it
- 1
Build the schema
Describe the app in App Studio — it generates the schemas and a reviewable UI — or author it over MCP from your editor. Aim at the object model above: a type opts into a service by extending its base type. Schemas are live on the CRUD API the moment they upload — there is no separate build step to run.
- 2
Configure the services
Imported services arrive uncredentialed. Put Stripe’s keys in the keystore for the charge you drive, plus email for renewal notices — see Email & Notifications. Remember the renewal loop itself is a workflow you trigger, not a scheduler the platform runs.
What you write yourself
⚠️ There is no recurring charge loop
next_billing_at, current_period_end and auto_renew are fields the platform stores and never acts on. The renewal loop is a workflow you define, and until you define it, subscriptions sit at whatever status they were last set to.| Decision | Why it is yours |
|---|---|
| Proration on plan change | The base plan holds one billing_amount and one period. Splitting a period across two prices is arithmetic you write. |
| What a lapsed member can do | Status is stored faithfully. Whether an expired subscription still books a class is an access rule you define. |
| Class capacity | class.max_capacity is a field to check against, not a lock the platform takes. Two bookings can pass the same check. |
| Which plan changes are legal | The service rejects illegal status transitions. It has no opinion on Basic jumping to VIP mid-period. |
Deployment
- 1
Preview while you iterate
A preview deploy answers at
https://preview-{domain}-{project}.supero.livefor a short lease while you build and test. Publish Your App covers how preview and live differ. - 2
Go Live
Go Live publishes to a permanent, Supero-hosted URL —
https://live-{domain}-{project}.supero.live— with no cloud account to connect and nothing to provision. It is asynchronous: you get a202and a poll URL, then the live address. The full contract is in Publish Your App. - 3
Watch the logs
When a deploy fails or the live app misbehaves, the **Cloud Deploy Logs** page in the dashboard shows the published service’s container logs — pick the Live lane to read the published app rather than the preview build (up to ~24 hours back, ~500 lines per request). For platform activity — requests, auth, errors across your domain —
GET /api/v1/logsis a filtered read, and a single request traces across services at/api/v1/logs/trace/{correlation_id}when you set your ownX-Correlation-ID. See Operations & Observability.
Final Thoughts
next_billing_at, current_period_end and auto_renew are fields the platform stores and never acts on. The renewal loop is a workflow you define, and until you define it, subscriptions sit at whatever status they were last set to.schemas.py, ui_schemas.py, ui/app.js, setup.py and run.sh that you can run and keep.Where to Go Next
Deeper
Subscriptions & Memberships
The service reference for plans, memberships and their operations. Open
The renewal loop
Workflows & Automation
How to define the renewal, dunning and reminder logic this page says is yours. Open
Payments
Managed Integrations
Stripe, PayPal, Razorpay and SendGrid, with credentials in the keystore. Open
Worked example
A Subscription Business, End to End
The same gym, followed from empty project to running application. Read it
On this page