Embedding
Multi-Tenancy
How to enable multi-tenancy for On-Premise installation or embedded studio
Overview
Whether you need multi-tenant separation in Carbone depends on how you execute Carbone On-Premise. Below is a non-exhaustive list of use cases indicating when multi-tenancy separation may be required:
| Execution Mode | Template source | Tenant boundary | Carbone separation needed? |
|---|---|---|---|
| Stateless (Ephemeral templates) | The template is sent with every render request (e.g., base64) via POST /render/template. Nothing is stored by the Carbone backend. |
Your application must enforce tenant separation (auth, access control, routing). | Not required in Carbone. Carbone acts as a stateless generator. |
| Hybrid (Template storage only) | Templates are stored as files (by Carbone or by your app), optionally on S3/other storage. Carbone does not manage tenant metadata. | Your application maintains the association template ↔ tenantId and enforces tenant access control. |
Usually not required in Carbone. Optional separation can be done at the storage layer (see below). |
| Stateful (Template versioning) | Carbone enables its SQLite database to support features like template listing, search, versioning, deployment. Carbone manages template storage (disk and/or S3) and stores template metadata in SQLite (deployed version, name, comments, tags, category…). | Carbone should enforce tenant separation because Carbone is now the system of record for template metadata and discovery features. | Recommended to separate by tenant in Carbone (e.g., to list/search templates per tenant safely). |
Set the tenantId
To enable tenant isolation, set req.tenantId in the HTTP request hook middleware plugin.
This is also the place where you can authenticate your users by reading a cookie or relying on an external authentication service such as Google OpenID, Microsoft Azure AD, Okta, Auth0, OneLogin, Keycloak, Ory, and others.
// HTTP middleware called before all routes
function beforeMiddleware(req, res, next) {
// Provide the tenant id using your own logic:
// header from your application, a token, external auth service, etc.
req.tenantId = req.headers['custom-tenant-id'];
return next();
}
Isolation behavior
- Template storage:
- without storage plugins: template files use the pattern
tenantId-versionIdautomatically. - with custom storage plugins: you must enforce tenant separation by reusing
req.tenantIdpreviously set (naming convention, per-tenant prefix, bucket separation, etc.) inwriteTemplate,readTemplate, ordeleteTemplatefunctions.
- without storage plugins: template files use the pattern
- SQLite Database (optional): rows are automatically isolated by
tenantId. - API behavior: all endpoints become tenant-aware (e.g.
GET /templatereturns only templates for the current tenant).
Constraints
req.tenantIdmust be a non-negative signed 64-bit integer.- Because JavaScript integers are only safe up to 53 bits, you can pass
tenantIdas a string to support the full 64-bit range: from1to9223372036854775807 - If the value is invalid, Carbone returns an error.
If tenantId is undefined or null, Carbone automatically disables tenant isolation. In this case, all data in the SQLite database will be stored with tenantId = 0 (see Security below).
Security
When req.tenantId is not set (i.e., it is undefined or null), Carbone will NOT separate data between tenants. This can be a security risk if you expect data isolation.
To force tenant isolation for all API requests and block requests where req.tenantId is missing (for example, due to a client mistake or misconfigured authentication), set the security level configuration:
- Set the environment variable
CARBONE_SECURITY_LEVEL=2. - Or, in your config file, set
securityLevel: 2
2 is a bitfield (0b00000010) that specifically enables tenantId isolation enforcement.
This enables an additional security check:
If a request does not include a valid tenantId, Carbone will reject it instead of disabling isolation.