Running under a control layer
Manablox can run under an external control layer: your own hosting panel, billing system or provisioning script. The layer decides what each instance may do (plans, prices, trials and customer accounts live there, never in the CMS), and tells the instance through the control API. The instance stores the values, enforces them and reports back.
This page walks through the setup in the order you need it. The details are in Controls (what each control does) and the Control API reference (every endpoint).
One instance serves one customer. Nothing about the layer changes a self-hosted instance:
without the control scope and a key nothing is mounted, and an instance without control
settings behaves as it always did.
1. Mount the control API on an internal port
The control API is the server scope control. It is in no preset, so a process serves it
only when SCOPES names it, and only with a key. Run it as its own process on a port that
only the control layer reaches, next to the management API of the same instance:
api-control: image: ghcr.io/manablox/cms-api:0.50.0 environment: SCOPES: control PORT: "3300" DATABASE_URL: ${DATABASE_URL} REDIS_URL: ${REDIS_URL} AUTH_SECRET: ${AUTH_SECRET} CONTROL_API_KEY: ${CONTROL_API_KEY} CONTROL_API_ALLOWED_IPS: 10.0.0.0/8 CONTROL_PROVISIONED: "true" TRUSTED_PROXIES: noneKeep the port off the internet and off the proxy that serves the admin. Share REDIS_URL
with the other processes of the instance: a control write then reaches every process at
once, and usage counts, rate limits and levels are shared.
2. Keys and client addresses
Every call sends Authorization: Bearer <CONTROL_API_KEY>. To rotate, set the new key as
CONTROL_API_KEY_NEXT, switch the layer over, then make it the only key. See
Authentication.
CONTROL_API_ALLOWED_IPS narrows the callers further. It reads the client address like
the rate limiter does, so set TRUSTED_PROXIES on every process: your proxies’ ranges, or
none where nothing sits in front. See Client addresses.
3. Provision the instance
Set CONTROL_PROVISIONED=true on every process from the first start: sign-up is closed,
the admin shows no install wizard, and no account becomes superadmin on its own. Then:
- Wait until
GET /control/v1/instanceanswers"health": { "status": "ok" }with no pending migrations. - Apply the plan:
PUT /control/v1/settings?scope=instancewith the features, limits, usage limits, rate limits and retention the customer gets.PUTreplaces the whole set atomically, so the same call applies a plan change later. - Optionally create spaces:
POST /control/v1/spaceswith a starter, locales, a group and API hosts. - Create the owner:
POST /control/v1/users/ownerwith the customer’s email and name and anIdempotency-Key. Send the returnedsetPasswordLink.urlyourself, or pass"sendMail": truewhen the instance has mail.
The owner is a superadmin, owner of every space, and can do everything a self-hosted superadmin can, except change control values: those are read-only in the admin for everyone. See Provisioning a new instance.
Settings apply to three scopes: the instance, a group of spaces (group:<id> or
group:ext:<your id>) and one space. Groups exist only in the control API; editors never
see them. See Scopes.
4. Receive events
The instance records what the layer may want to react to (a space created, a seat added, a usage threshold reached, a limit refusing an action, a domain verified, a snapshot taken) in an outbox, in the same transaction as the cause. Read it either way, or both:
- Push: set
CONTROL_WEBHOOK_URLandCONTROL_WEBHOOK_SECRET. The instance posts batches in order, signed withX-Manablox-Signature, and retries with backoff. Answer 2xx to acknowledge. - Pull: call
GET /control/v1/events?after=<seq>and storenext. Events are kept forretention.controlEventsDays(30 days by default).
Delivery is at least once, so deduplicate by id or seq. See
Events.
5. Count usage, including your CDN
The instance counts API requests, bandwidth, workflow runs, form submissions, mails, AI
calls and uploads per space and period, whether or not a limit is set. Read them with
GET /control/v1/usage?scope=&period=, and align periods with your billing day through
usagePeriodAnchorDay.
Requests a CDN answers from its own cache never reach the instance. Report them with
POST /control/v1/usage/external (by space id or host, with your own idempotency key),
and send only the cache hits: every request that reaches the instance is counted
already. See Usage.
Usage limits (usage.<metric>) then refuse or only report once a period’s usage passes
them, depending on their mode. See Usage limits.
6. Set the state
PUT /control/v1/instance/state switches the whole instance:
| Status | For example | Effect |
|---|---|---|
active | Normal operation | Nothing is restricted |
readOnly | An unpaid invoice | Writes are refused; delivery and sites keep serving |
suspended | A cancelled account | The admin shows only your message; delivery and sites answer 503 |
The control API, sign-in and the health endpoints keep working in every state, so the same
call lifts it again. A space or a group can be set read-only through the settings
endpoints, for example a space over its limits after a downgrade. Suspending and lifting a
suspension purge every cached delivery and site page through the cache:purge hook, so a
CDN purge handler (see Caching) drops them too. See
Instance state.
7. Serve several spaces and custom domains
- Public API: one unpinned public API process serves every space of the instance, each on its own API hosts (
POST /spaceswithapiHosts,PUT /spaces/<id>/api-hosts). See Which space a request reads. - Domains: with a shared proxy that issues certificates on demand, switch on
domains.requireVerification, so a customer’s new domain serves only after a DNS record proves ownership. SetDOMAIN_CNAME_TARGETto let customersCNAMEto your proxy instead. See Custom domain verification. - Links: set
admin.links(upgrade,billing,support,docs) so locked features and usage banners send people to your pages, andadmin.bannersfor notices. See Admin messages.
8. Staging environments
A space can have staging environments next to production, where the customer changes
the content model, designs and content before promoting them. Offer them per plan with
features.environments and limits.environmentsPerSpace (staging environments per
space; production is not counted, so 0 allows none). Staging rows count toward no other
limit; a promote checks production’s limits for what it adds.
The layer can list, create, diff, promote and delete environments through
/control/v1/spaces/<id>/environments, and hears about them through the
environment.created, environment.promoted and environment.deleted events. While a
promote runs, writes to the space’s production environment answer 423
control.readOnly with the reason promote; reads, delivery and sites keep serving. See
Environments and
Environments in Controls.
9. Move a customer from SQLite to Postgres
A small plan can run on SQLite and a larger one on Postgres. To move an instance, set it
read-only, run manablox migrate-db --to <postgres-url>, switch DATABASE_URL, start it
and lift read-only. Everything moves, ids and links included. See
Moving from SQLite to Postgres. On Postgres, run the migrations
with an owner role and the CMS with an app role that cannot rewrite the audit log; see
Database roles.
What stays in your layer
Plans, prices, trials, billing, invoices, customer accounts and sign-up, creating the instances themselves (containers, databases, domains, CDN), backups of whole instances, translating plans into control values, reacting to events, and CDN log processing. The CMS knows features, limits, usage, rate limits, retention, messages and events, and nothing else.