Base 3:16
Concepts

How the model fits together

Environments, customers, plans, features and entitlements — and which of them is yours.

Read this before designing a real catalog. Most integration trouble comes from mapping these onto the wrong things.

The shape

Organization          your company
└── Project           one of your products
    └── Environment   sandbox | production
        ├── Customer        an account you bill
        ├── Meter           a raw thing you count
        ├── Feature         something a customer buys
        ├── Plan            a price, and what it entitles
        │   └── PlanVersion immutable, so terms never change under a subscriber
        └── Subscription    a customer on a plan version

Everything below Environment is scoped to it. That is the isolation boundary, and it is enforced on every read: an id from a request body is always resolved inside the caller's environment first, so one tenant cannot reach another's data by guessing.

Customer, and your own id

A Customer is an account you bill. It carries a Base316 id (cus_…) and your own externalId.

Use externalId everywhere. It is the id your systems already have, it means the same thing on both sides, and it makes an import re-runnable. Quote the cus_… id on a support call — externalId is unique per environment, so the same value legitimately exists in sandbox and production.

Every customer has an owner

A customer belongs to one of:

  • a tenant — a runtime organization (runtimeOrganizationId, or its own externalId as runtimeOrganizationExternalId);
  • a person — a runtime user (runtimeUserId), for a solo end user;
  • a tenant in your own system — its externalId, when you run your own auth and use Base316 for billing.

A customer with none of these is refused unless you create it with standalone: true, which says it deliberately belongs to nobody — a walk-up customer on a service invoice, for instance. The rule exists because an unowned customer is how one account ends up with two: the one your checks read, and the one somebody upgraded.

A customer linked to a tenant carries the tenant's externalId or none, so the two can never disagree. And a customer has one live subscription: a second is refused (409) while the first is trialing, active or past due. Change its plan instead.

Accounts created before this rule can still be unowned, doubled up, or carrying two live subscriptions. GET /v1/customers/health lists them — so does the card at the top of Customers in the dashboard — and POST /v1/customers/{id}/merge (after /merge-preview) folds one account into another: subscriptions, credits, wallet balance and unbilled usage move, while issued invoices and payments stay on the merged account, which no longer resolves. A customer.merged webhook names both.

To check access for a tenant without knowing its customer, pass organizationId or organizationExternalId to check() — it resolves to the same customer the dashboard shows and the user's token describes.

Feature versus meter

This is the distinction that trips people up.

  • A meter is a raw thing you count: api_call, tokens_used, seat_assigned. It aggregates events.
  • A feature is something a customer buys: api_calls, advanced_reports. A metered feature is backed by a meter; a boolean feature is not.

You check features. You record events, which meters aggregate. Your application never asks about a meter directly.

Plan versions are immutable

Editing a price would silently re-price everybody already on it. So a plan has versions, a subscription points at one, and publishing a new version leaves existing subscribers exactly where they were until you move them. See Move subscribers to a new plan version.

Where identity fits

Alongside the billing graph is a runtime identity graph: your application's own users, organisations, roles and permissions. RuntimeUser is your end user; Customer is the account that pays.

They are linked, and that link matters: a permission check (check({ userId, permission })) can only answer NOT_A_MEMBER for an account with no tenant behind it. Entitlement checks work without it.

The account is opened for you

Creating a tenant — POST /v1/organizations, or an end user signing up through a hosted page — also opens a Customer linked to it. Without that, a tenant exists that billing cannot see: nothing to subscribe, and every permission check against it answering NOT_A_MEMBER.

It does not get in your way if you create your own. The account opened for a tenant is a placeholder until something uses it, so the usual pattern still works unchanged:

POST /v1/organizations   { "name": "Globex", "externalId": "org_42" }
POST /v1/customers       { "name": "Globex Inc", "externalId": "org_42" }

The second call fills in the account already opened rather than colliding with it — externalId is unique per environment and the tenant link is unique outright, so a second row is not possible. You get one account, with your details, already linked.

An account you created before its tenant is adopted the same way, matching on the id both carry. One that already bills a different tenant is left alone, because taking a unique link would move somebody's billing without saying so.

This is on for every environment, and there is no setting to turn it off yet. An integration that provisions its own customers loses nothing to it — its customer claims the account opened for the tenant, as above. One that meters without tenants never creates a tenant, so nothing is opened.

Tenants that run other tenants

A tenant can be created under another with parentId — a firm over its client companies, an agency over its brands. One level deep: a managed tenant cannot be a parent.

Hartley & Co            parent: its own members, plan and invoice
├── Acme Ltd            managed: its own members and data, billed to the firm
└── Globex Inc          managed

Three things follow, each covered in Managed tenants:

  • Access. A parent role mapped to a child role lets the firm's staff act inside every client. Their direct memberships always win; an unmapped role reaches nothing.
  • Billing. The client's customer bills to the firm's (billToCustomerId): the firm's plan entitles the client, the allowance is shared, and the client's usage lands on the firm's invoice, broken down per client.
  • Brand. The client's hosted pages and the components show the firm's branding unless the client sets its own.

What Base316 does not interpret

A permission is a string your code checks for. Base316 stores reports.export, matches it, and has no opinion about what it means. Declaring one in the dashboard gives it a name and makes it pickable rather than retyped — it does not grant it. A role grants it.

On this page