S
supero.docs
Documentation/Application Architectures/Membership & Recurring Billing
Solution Architecture

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.

A membership architecture: plan, subscription, member and payment types extending platform base types
Publish this elsewhere:

The Project

This is the shape for any business where a customer holds an ongoing entitlement rather than buying a thing once: a gym, a club, a studio, a co-working space, a professional body, a support plan. The defining question is not "what did they buy" but "are they currently in good standing, and until when".
The exemplar behind this page is a real generated application, 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.
A membership business rarely stays purely one. The gym also sells classes, takes orders through a cart, and collects a signed waiver — that composes, because it all extends the same base types.

Architecture & Design

The architecture turns on one line: a type earns its lifecycle by extending a platform base type.
A schema joins a platform service by extending that service’s base type. Your schema keeps its own name, namespace and fields; the base type contributes the lifecycle fields and the state machine the service drives.
python
{"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

On upload, 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.
These are the extending types in the gym bundle and what each one inherits:
Your typeExtendsWhat the base type brings
membership_planrecurring_plan:base_planstatus (inactive, active, paused, cancelled, expired), billing_interval, billing_amount, currency, current_period_start, current_period_end, next_billing_at, external_billing_id
member_subscriptionmembership:base_membershipstatus, role, joined_at, suspended_at, expires_at, suspension_reason, expulsion_reason, transfer_target_uuid
member_profilecustomer:base_customerCustomer identity, parented to user_account rather than to the tenant
paymentpayment:base_paymentstatus (pending, authorized, captured, refunded, voided, failed), captured_amount, refunded_amount, external_payment_id
order, order_itemorder:base_order, order:base_order_itemTotals, item_count, source_cart_uuid, and the pending-to-fulfilled path
cart, cart_itemcart:base_cart, cart:base_cart_itemLine items held before checkout
waiver, waiver_signaturedocument_signature:base_document and :base_signaturerequired_signatures, completed_signatures, signature status
The gym’s other types are 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. 1

    Publish the plans

    Create membership_plan records. billing_amount, currency and billing_interval come from recurring_plan:base_plan; category and benefits are yours. is_active controls whether the plan is offered.

  2. 2

    A member joins

    A member_profile is created — it extends customer:base_customer, so it is parented to a user account, not to the tenant directly. Then a member_subscription referencing the chosen plan.

  3. 3

    Take the money

    The gym imports stripe_checkout. Its credentials — stripe_api_key and stripe_webhook_secret — live in the keystore at /api/v1/keystore/platform-secrets and read back masked, never in your code.

  4. 4

    Record the payment

    A payment record carries the result. external_payment_id and external_charge_id are what tie it back to the processor.

  5. 5

    Renewal comes round

    This step is yours to build. next_billing_at is a field on the plan, and a workflow you define reads it, charges, and advances the period. Nothing charges on its own.

  6. 6

    Suspend, cancel or expire

    Every transition goes through one endpoint. An illegal move returns 409 INVALID_STATE_TRANSITION rather than quietly writing a bad status.

That single endpoint is worth stating plainly, because it is the same one for every service in the platform. There is no cart API, no membership API and no state-machine API:
json
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_user and viewer; platform_admin and platform_user are 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.py and run.sh, which is the whole bundle you can run and keep.

💡 Read the bundle, not just the docs

The imported base types are in 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

Two kinds of work: first you model the shape above and point each type at its base type, then you build the parts the base types deliberately leave to you.

Model it

  1. 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. 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

The most important thing to know before planning a membership build. Supero runs no billing scheduler: it does not charge on a date, retry a failed card, dun, or send renewal reminders. 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.
Free trials are the same story. There is no trial concept on the base plan — a trial is a status and a date you manage yourself, plus the workflow that ends it. Beyond billing, these are the decisions the exemplar leaves open:
DecisionWhy it is yours
Proration on plan changeThe base plan holds one billing_amount and one period. Splitting a period across two prices is arithmetic you write.
What a lapsed member can doStatus is stored faithfully. Whether an expired subscription still books a class is an access rule you define.
Class capacityclass.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 legalThe service rejects illegal status transitions. It has no opinion on Basic jumping to VIP mid-period.
None of it is much work. It is the part genuinely specific to your business, which is why it is not pre-decided — but plan for it rather than discovering it late. For the complete set, see Roles & Permissions Reference.

Deployment

The path from this shape to a running membership business at a public URL, end to end:
  1. 1

    Preview while you iterate

    A preview deploy answers at https://preview-{domain}-{project}.supero.live for a short lease while you build and test. Publish Your App covers how preview and live differ.

  2. 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 a 202 and a poll URL, then the live address. The full contract is in Publish Your App.

  3. 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/logs is a filtered read, and a single request traces across services at /api/v1/logs/trace/{correlation_id} when you set your own X-Correlation-ID. See Operations & Observability.

Final Thoughts

This shape fits a business where a customer holds an ongoing entitlement rather than buying a thing once — a gym, a club, a studio, a co-working space, a professional body, a support plan. The defining question is not what they bought but whether they are currently in good standing, and until when. A membership business rarely stays purely one, and that composes: the same gym also sells classes, takes orders through a cart and collects a signed waiver, because it all extends the same base types.
If you take one thing into the build, take this: there is no recurring charge loop. Supero runs no billing scheduler — it does not charge on a date, retry a failed card, dun, or send renewal reminders. 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.
And what you walk away with is yours. The lifecycle fields and state machines the platform services bring, the keystore-held Stripe, SendGrid and S3 credentials, and the fixed role set stay the platform's. What you own is the schema, the access rules, and the renewal, proration and trial logic specific to your business — delivered as the generated bundle of schemas.py, ui_schemas.py, ui/app.js, setup.py and run.sh that you can run and keep.