Skip to content

Tenants

Included free on every install, with the default tenant, domain mapping, the JSON Lines and dump migrate routes, and the cost ledger. Creating more tenants needs multitenant-provisioning, backup, restore and clone need tenant-backup, the database migrator needs migration-toolkit, and per-tenant admin branding needs multitenant-customization. See pricing.

A tenant is one customer, brand or site on a shared instance. Each tenant has its own content types, entries, media, users, API keys and settings, and a request in one tenant never reads another tenant's data. A free install runs one tenant, default. With a license you create as many as your plan allows.

Every request runs in exactly one tenant, decided before it is handled:

CallerTenant the request runs in
A signed-in userTheir home tenant, or the tenant they named when signing in, if they hold a membership there.
An API keyThe tenant the key was created in.
An admin tokenThe tenant the token was issued for. An X-Tenant-ID naming another answers 403.
A super adminTheir own tenant, or any tenant they name with X-Tenant-ID. An unknown or disabled slug answers 404 with {"error":"tenant not found"}.
A request with no credentialThe tenant mapped to the Host header's domain. With one tenant on the install, that tenant. With MULTI_TENANT=true, several tenants and no mapping for the host, 404.

X-Tenant-ID from anyone but a super admin is ignored, so a client cannot reach another tenant by sending one. Every read and write is then limited to the resolved tenant.

This creates a second tenant, writes an entry into it and shows that the default tenant cannot see it. Creating a tenant needs a license with multitenant-provisioning. On a free install, step 2 answers 402. You need a super admin token in TOKEN. The quickstart shows how to get one.

  1. Check that this install may create tenants:

    Terminal window
    curl http://localhost:3001/api/admin/tenants/provisioning \
    -H "Authorization: Bearer $TOKEN"
    {"enabled": true}

    A free install answers {"enabled": false}.

  2. Create the tenant:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/tenants \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"slug": "acme", "name": "Acme Inc."}'

    The answer is 201 with the tenant. plan defaults to free.

    {
    "id": "3f64a521-1146-481c-aaf8-a3334f505aa3",
    "slug": "acme",
    "name": "Acme Inc.",
    "plan": "free",
    "enabled": true,
    "created_at": "2026-10-02T05:05:55.15037Z",
    "updated_at": "2026-10-02T05:05:55.15037Z",
    "archived": false
    }
  3. Define a content type inside acme. As a super admin, X-Tenant-ID picks the tenant:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/schemas \
    -H "Authorization: Bearer $TOKEN" \
    -H "X-Tenant-ID: acme" \
    -H "Content-Type: application/json" \
    -d '{"name": "note", "fields": [{"name": "title", "field_type": "text", "required": true}]}'

    The answer is 200 with the stored definition.

  4. Write an entry into acme:

    Terminal window
    curl -X POST http://localhost:3002/api/v1/content/note \
    -H "Authorization: Bearer $TOKEN" \
    -H "X-Tenant-ID: acme" \
    -H "Content-Type: application/json" \
    -d '{"data": {"title": "Only in Acme"}}'

    The answer is 201 with the entry.

  5. Read note from each tenant:

    Terminal window
    curl http://localhost:3002/api/v1/content/note \
    -H "Authorization: Bearer $TOKEN" -H "X-Tenant-ID: acme"
    curl http://localhost:3002/api/v1/content/note \
    -H "Authorization: Bearer $TOKEN" -H "X-Tenant-ID: default"

    The first lists the entry. The second answers 404, because the note content type exists only in acme.

  6. Open Operations > Tenants in the admin console. acme is in the list, and its page shows its members and the features it may use.

Set MULTI_TENANT=true before you add a second tenant. An account or API key acts in the tenant it belongs to either way. The setting adds what a public site needs on a shared instance: a request with no credential on a host that maps to no tenant answers 404 once the install holds several tenants.

With MULTI_TENANT=true, first-run setup creates the default tenant and puts the first super admin in it. An install that turns the setting on later creates default itself with POST /api/admin/tenants and the slug default, which needs no license.

Tenant routes need the super_admin role. Anyone else gets 403, with insufficient permissions on the tenant routes and super_admin role required on the domain and membership routes. In the console a super admin works under Operations > Tenants.

A slug is how every route, header and domain mapping names a tenant:

RuleValue
Pattern^[a-z][a-z0-9_]{0,62}$
First characterA lower-case letter
Other charactersLower-case letters, digits and underscores. No hyphens.
Length1 to 63 characters

PUT /api/admin/tenants/{id} changes name, plan or enabled, and fields you leave out keep their value. A disabled tenant is refused as an X-Tenant-ID target, and on a public request it no longer counts as a tenant to serve.

The license sets how many tenants you may hold. Past that, a create answers 402 with tenant quota reached (<n>); upgrade your plan to add more tenants. If a license lapses, every tenant you already have stays readable, editable, archivable and deletable. Only creating new ones is refused.

A slug that belonged to a deleted tenant whose rows are still stored answers 409 and names the tables that hold them, so a new tenant never inherits old data. Purge those rows or choose another slug.

  • Archive (POST /api/admin/tenants/{id}/archive) makes a tenant read-only and keeps its data. Every write to it answers 423 with {"error":"tenant is archived - read-only"}. Reads still work.
  • Restore (POST /api/admin/tenants/{id}/restore) makes it writable again.
  • Delete (DELETE /api/admin/tenants/{id}) removes everything the tenant holds, across every feature, in one transaction. If any part fails, nothing is deleted and the tenant stays. The answer is 204. There is no undo.

These routes take a signed-in session. An admin token or an API key cannot call them. Archive a tenant if you may need it back. Take a backup first if you need its content after it is gone.

Map a hostname to a tenant, and a request to that host runs in that tenant without any credential naming it. This is how a public site reaches its own content on an install with several tenants. A mapping resolves only once it is verified: send "verified": true when you have already proved control of the domain, or set it later with PUT. These routes need a super admin.

Terminal window
curl -X POST http://localhost:3001/api/admin/tenant-domains \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"domain": "cms.acme.com", "tenant_id": "acme", "verified": true}'

The answer is 201 with the mapping. tenant_id is the tenant's slug, and domain is a hostname with no scheme and no port. A domain mapped to another tenant answers 409.

An account belongs to the tenant it was created in, its home tenant. A super admin can grant it roles in other tenants:

Terminal window
curl -X PUT http://localhost:3001/api/admin/users/$USER_ID/memberships \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"tenant_id": "acme", "roles": ["editor"]}'

The account then signs in to that tenant by naming it:

Terminal window
curl -X POST http://localhost:3001/api/admin/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "editor@example.com", "password": "<password>", "tenant": "acme"}'

Without tenant, a sign-in acts in the home tenant. A tenant the account holds no membership in answers 401, the same as a wrong password. POST /api/v1/auth/token takes the same field. A signed-in account lists the tenants it may act in with GET /api/admin/auth/memberships, which is how a client builds a tenant switcher.

Granting and revoking need a super admin with a signed-in session. In the console, open a tenant under Operations > Tenants and use New member.

A super admin can withhold features from one tenant while the rest of the install keeps them, for example to keep a trial customer off the AI features. The license stays the ceiling, and an empty list gives the tenant everything it allows.

Terminal window
curl -X PUT http://localhost:3001/api/admin/tenant-features/acme \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"withheld": ["flow"]}'

The answer lists withheld and known, every feature name the instance can withhold. A request in acme to a withheld feature then answers 403 with This feature is not available to this tenant. A name the instance does not serve answers 422.

Paid capabilities can be withheld the same way. The tenant then meets the 402 an unlicensed install would, while every other tenant keeps the capability. alerts-pro is sold inside scheduled jobs, error tracking, logs and synthetic monitoring, so it is withheld by its own name, or by withholding all four of those features. Withholding only one of them leaves the tenant's alerts on the other three. These routes take a signed-in session. In the console, the tenant's page has What this tenant may use.

The JSON Lines and portable dump routes under /api/admin/migrate/ are free. An admin works on their own tenant there, and naming another tenant answers 403. An imported record keeps its id when it can, so relations survive a move.

The database migrator (migration-toolkit) copies a whole database from one connection string to another, in any pairing of PostgreSQL, MySQL and SQL Server. It copies tables and rows, not constraints, keys or indexes, runs for at most 30 minutes per job, and a rollback drops only the tables the job created. Private, loopback, link-local and cloud metadata addresses are refused unless MIGRATION_ALLOWED_PRIVATE_NETWORKS lists them.

The cost ledger records what each tenant consumes, priced, by resource type: database, storage, bandwidth, compute and ai. Every hour it reads the instance's own meters and writes one line per meter for the current and the previous month, updating those lines rather than adding new ones:

  • API requests become compute, and bytes sent become bandwidth.
  • The monthly usage snapshot becomes storage.
  • AI usage becomes ai, at the cost recorded per model.

You can also post entries by hand. Until you set a tenant's own price on the prices routes, these defaults apply, in USD: compute 1.00 per million requests, bandwidth 0.09 per GiB, storage 0.023 per GiB. database and ai default to zero.

A budget names a resource type (or all), a period (monthly, quarterly or annual), an amount and percentage thresholds. When spend crosses a threshold, the cost.budget.exceeded event fires once per budget, period and threshold, so a flow can notify someone. Its data carries tenant_id, budget_id, budget_name, resource_type, period, period_start, period_end, amount, budget_amount, threshold and currency.

Every cost route needs the admin role and works on the caller's tenant. In the console, admins open Operations > Tenant costs.

Requires a license with the multitenant-customization feature. See pricing.

A tenant's admin can make the admin console their own, under Settings > Customization. Everything is kept per tenant. The same license also gives each tenant its own captcha provider and its own metrics destination.

  • Brand. A display name of up to 60 characters in the header and tab title, an accent color as #RRGGBB, a welcome line of up to 280 characters, and a logo: a PNG, JPEG or WebP of up to 500 KB, stored in the media library.
  • Menu. Hide entries the team never uses, pin the daily ones at the top, and add links to the team's own tools, each optionally limited to roles. Hiding an entry does not change who may open the page behind it.
  • Dashboard. Up to 24 widgets in your order, each a third, half or full row wide and optionally limited to roles. A widget shows only what its viewer could open anyway.
  • Pages. Up to 50 pages of your own, each up to 40 blocks, optionally limited to roles. One page can open at the top of the dashboard.

Without the license, reads answer 200 with "entitled": false and the stock admin, page reads answer 404, and writes answer 402. Saved settings are kept and apply again when the license returns.

Page blocks and dashboard widgets
BlockShows
markdownFormatted text, up to 20,000 characters. Raw HTML is not rendered.
calloutA title and short text in a tone: neutral, brand, success, warn or danger.
contentThe latest 1 to 50 entries of a content type, optionally by status.
statsAn entry count for each of 1 to 12 content types.
linksA list of links.
WidgetShows
entry_countsEntry counts for 1 to 12 content types, with a recent trend.
latest_entriesThe 1 to 20 most recently changed entries.
status_breakdownHow many entries are published, draft and archived.
recent_activityThe latest 1 to 20 audit entries.
media_usageFiles in the media library, their size and kinds.
api_usageRequests, failure rate and latency over 6h, 24h or 7d. Needs API analytics.
linksA list of links.
textFormatted text of up to 20,000 characters.
VariableWhat it doesDefault
MULTI_TENANTtrue makes each account and API key act in its own tenant, and lets first-run setup create default.false
MIGRATION_ALLOWED_PRIVATE_NETWORKSComma-separated CIDR ranges or addresses the database migrator may connect to. It opens them to the migrator and to nothing else.none

Tenants run on every install. If you set LYEVE_PLUGINS to choose which features start, include multitenant. See Licensing and tiers.

Tenants, domains and memberships
MethodPathPurpose
GET/api/admin/tenantsList tenants by slug, paginated.
POST/api/admin/tenantsCreate a tenant: slug, name, optional plan. Needs multitenant-provisioning.
GET/api/admin/tenants/provisioning{"enabled": true} when this install may create tenants.
GET/api/admin/tenants/{id}One tenant.
PUT/api/admin/tenants/{id}Change name, plan or enabled.
DELETE/api/admin/tenants/{id}Delete a tenant and all its data. Session only.
POST/api/admin/tenants/{id}/archiveMake a tenant read-only. Session only.
POST/api/admin/tenants/{id}/restoreMake an archived tenant writable again. Session only.
POST/api/admin/tenants/{id}/cold-archiveWrite an archived tenant's record to storage. Session only.
GET/api/admin/tenants/{id}/archivesThe tenant's cold-storage archives.
POST/api/admin/tenants/{id}/archives/{archiveID}/restoreCheck a cold-storage archive and make the tenant writable. Session only.
GET, POST/api/admin/tenant-domainsList or create domain mappings: domain, tenant_id, verified.
GET, PUT, DELETE/api/admin/tenant-domains/{id}One mapping. PUT takes tenant_id and verified.
GET/api/admin/users/{id}/membershipsThe tenants an account may act in. Session only.
PUT/api/admin/users/{id}/membershipsGrant roles in a tenant: tenant_id, roles. Session only.
DELETE/api/admin/users/{id}/memberships/{tenant}Revoke a tenant. Session only.
GET/api/admin/tenant-members/{tenant}Every account that may act in a tenant. Session only.
GET, PUT/api/admin/tenant-features/{tenant}What is withheld from a tenant: {"withheld": [...]}. Session only.
GET/api/admin/auth/membershipsThe tenants the signed-in caller may act in. Any role.

Every route above except the last needs the super_admin role.

Tenant costs
MethodPathPurpose
GET, POST/api/admin/cost-monitor/entriesList entries (resource_type, from, to in RFC 3339, paginated) or add one.
GET, DELETE/api/admin/cost-monitor/entries/{id}One entry.
GET/api/admin/cost-monitor/summarySpend over from and to.
GET, POST/api/admin/cost-monitor/budgetsList or create budgets.
GET, PUT, DELETE/api/admin/cost-monitor/budgets/{id}One budget.
GET/api/admin/cost-monitor/budgets/alertsThresholds crossed.
GET/api/admin/cost-monitor/anomaliesUnusual spend, filtered by resource_type, severity and status.
POST/api/admin/cost-monitor/anomalies/detectLook for anomalies now.
GET, PATCH/api/admin/cost-monitor/anomalies/{id}One anomaly, or change its status.
GET/api/admin/cost-monitor/recommendationsWays to save, with the estimated saving.
POST/api/admin/cost-monitor/recommendations/generateGenerate recommendations now.
GET, PATCH/api/admin/cost-monitor/recommendations/{id}One recommendation, or change its status.
GET/api/admin/cost-monitor/dashboardThis month: spend so far, budgets at risk, open anomalies, top recommendations.
POST/api/admin/cost-monitor/aggregateRead the meters now instead of waiting for the hour.
GET/api/admin/cost-monitor/pricesThe price of each resource type, and whether it is yours or the default.
PUT, DELETE/api/admin/cost-monitor/prices/{resource_type}Set a price (unit_price, per_units, currency) or go back to the default.
Admin branding
MethodPathRolePurpose
GET/api/admin/customizationsigned inBrand, menu and pages, filtered to the caller's roles.
PUT/api/admin/customizationadminSave brand, menu and dashboard page. Up to 256 KB.
GET/api/admin/customization/pages/{slug}signed inOne page.
PUT/api/admin/customization/pages/{slug}adminCreate or replace a page. Up to 512 KB.
DELETE/api/admin/customization/pages/{slug}adminDelete a page.
GET/api/admin/customization/dashboardsigned inThe dashboard. "custom": false means the stock one.
PUT/api/admin/customization/dashboardadminReplace the dashboard, 1 to 24 widgets. Up to 256 KB.
DELETE/api/admin/customization/dashboardadminGo back to the stock dashboard.
StatusMessageCause
402payment_requiredCreating a tenant without multitenant-provisioning.
402tenant quota reached (<n>); upgrade your plan to add more tenantsThe plan's tenant ceiling is reached.
404tenant not foundAn X-Tenant-ID names a slug that is unknown or disabled.
423tenant is archived - read-onlyA write to an archived tenant.
Every tenant and domain error
StatusMessageCause
400slug and name are requiredA create field is missing.
400invalid slug: must be lowercase alphanumeric + underscore, 1-63 chars, starting with a letterBad slug.
400domain and tenant_id are requiredA mapping field is missing.
400domain must be a hostname, without a scheme or a portBad domain.
400no tenant holds that slugA mapping names an unknown tenant.
403insufficient permissionsA tenant route called without the super_admin role.
403super_admin role requiredA domain or membership route called without the super_admin role.
403This feature is not available to this tenant.The feature is withheld from the caller's tenant.
404tenant not found or domain mapping not foundUnknown id.
409tenant slug already existsChoose another slug.
409slug "<slug>" still holds data from a deleted tenant in: ...Purge those rows or choose another slug.
409that domain is already mapped to a tenantRemove the other mapping first.