Run multi-tenant SaaS applications
Serve every tenant of a SaaS product from one function that resolves the tenant from the hostname, with configuration in KV Store and data in SQL Database.
A SaaS product team serves many customers, each on its own hostname, and must onboard tenants without a deployment per tenant. Every tenant shares one deployment, so a tenant’s configuration and data must never reach another tenant’s visitors. This page runs the product as one function that resolves the tenant from the hostname of each request, reads the tenant’s configuration from KV Store, and reads only that tenant’s rows from SQL Database. Tenants live on subdomains of the product’s domain, under one Let’s Encrypt wildcard certificate, so onboarding a tenant is a data change and a domain change. The result is measured by the time to onboard a tenant with its hostname, zero cross-tenant data exposure, and response time per tenant.
This use case does not cover running code supplied by tenants.
Prerequisites
- An application with Application Accelerator on, served by a workload on the production infrastructure. To create them, refer to Applications quickstart, and to turn on Application Accelerator, refer to Turn on Application Accelerator. The rule on this page answers every path with the function.
- The product’s domain as an active zone in Edge DNS, which the wildcard certificate needs for automatic issuance.
- A KV Store namespace for tenant configuration. To create one, refer to Namespaces.
- A SQL Database database for tenant data. To create one, refer to Databases and queries.
- A personal token, for the API calls. To create one, refer to Personal tokens.
- The values of your product. This page uses
example.comfor the zone,*.app.example.comfor the tenant hostnames,acme.app.example.comandglobex.app.example.comfor two tenants with the IDs1and2,saas-tenantsfor the namespace,saas-datafor the database, and aprojectstable as the tenant data. Replace each value with yours in every step.
Required products
| The product needs | Which means | Product | Documented in |
|---|---|---|---|
| HTTPS on every tenant hostname, with no certificate per tenant | A Let’s Encrypt wildcard certificate for *.app.example.com, bound to the workload | Certificate Manager | Request a wildcard certificate |
| The tenant resolved on every request | A function that reads the hostname of the request and runs on every path | Functions | Functions quickstart |
| Tenant configuration that onboarding writes with no deployment | One key per tenant hostname in a namespace | KV Store | KV Store API |
| Tenant data that no other tenant can read | Rows keyed by an integer tenant ID, read with that ID bound in every query | SQL Database | SQL Database API |
| A rule that sends every path to the function | The Run Function behavior, which requires Application Accelerator | Application Accelerator | Run a function on an application |
Reference architecture
This page builds the Pooled multi-tenant application: every tenant hostname reaches the same workload and the same function, and isolation lives in the code and in the data keys.
Read the diagram from the visitor down. Every tenant reaches the same workload, application, and function, so nothing in the request path names a tenant until the function reads the hostname. From there, isolation is a chain of keys: the hostname selects the configuration in KV Store, the configuration carries the tenant ID, and the tenant ID is bound in every query to SQL Database. Onboarding, at the bottom, writes data and a domain, and it touches no code and no deployment.
Dataflow
- A visitor requests a tenant hostname, and the workload completes the TLS handshake with the wildcard certificate that Certificate Manager issued for it.
- The application’s rule runs the
tenant-appfunction on every path. - The function reads the hostname of the request and looks up the key
tenant:<hostname>in thesaas-tenantsnamespace. A hostname with no key answers404. - The tenant’s configuration carries its integer ID, and the function binds that ID in its query to
saas-data, so the result holds only that tenant’s rows. - The function answers with the tenant’s data. A cached response is keyed by the request, which carries the hostname, so it never answers another tenant.
- Onboarding a tenant writes its rows and its key, adds its hostname to the workload, and points the hostname at the workload. The tenant answers once the change propagates, with no deployment.
Components
- workload: the Platform Resource that carries the tenant domains. Every tenant hostname is listed on it in full, because a workload refuses a wildcard entry, and its one deployment sends all of them to the same application.
- Certificate Manager: the Platform Resource that issues and renews the Let’s Encrypt certificates for the tenant hostnames. A wildcard certificate for the tenants’ parent domain, validated through DNS-01 in Edge DNS, covers a new tenant hostname with no new certificate.
- Functions: resolve the tenant from the hostname and run the product’s logic. Isolation lives in this code, so every query takes the tenant ID from the configuration, never from the request.
- KV Store: holds the tenant configuration, one key per hostname, which a function writes and reads. A new key is visible everywhere within 60 seconds.
- SQL Database: holds the tenant data, isolated by an integer tenant key bound in every query, or by one database per tenant as a design option. A function reads it on its read replica, and writes go through the Azion API.
- Cache: stores responses keyed by tenant. The default cache key carries the host, and a function’s Cache API entry is keyed by the request, so a cached response stays with its tenant.
- application: the Platform Resource that routes every path to the tenant function. The Run Function behavior needs Application Accelerator on the application.
Other designs for this use case
- Siloed multi-tenant application: for SaaS products whose tenants need dedicated resources, for compliance or custom configuration. A provisioning pipeline creates each tenant’s workload, application, functions, and stores from one template through the Azion API or the Terraform Provider, so isolation comes from separation instead of code, and updates roll out tenant by tenant.
Configure the tenant certificate
Every tenant hostname is a subdomain of app.example.com, so one wildcard certificate covers all of them, and a new tenant needs no certificate of its own. Azion issues a wildcard certificate only through the DNS-01 challenge, and it inserts the challenge record itself when the zone is active in Edge DNS. A workload’s form requests certificates only for the hostnames it lists, and it cannot list a wildcard, so the request goes through the API.
The certificate is requested, checked, and bound as Request a wildcard certificate describes, with these values:
-
Request body: the zone
example.comis active in Edge DNS, so the DNS-01 challenge needs no record from you. -
Binding:
tenants-wildcard, selected under My certificates in the Digital Certificate field of the workload that serves the application. -
Workload domains: each tenant hostname, such as
acme.app.example.com, listed in full, because a workload refuses a wildcard entry. A workload lists up to 50 domains unless Azion raises the bound.
The Console shows “Your workload has been updated”. Once the binding propagates, the certificate reads active, and it covers every hostname under app.example.com that the workload lists. Azion renews it before it expires, as long as the zone stays in Edge DNS.
Configure the tenant application
The tenant application is one function, tenant-app, that runs on every path. It does two jobs. On a tenant hostname, it resolves the tenant and answers with that tenant’s projects. On PUT /_admin/tenants/<hostname>, it writes a tenant’s configuration, which is how onboarding reaches KV Store: a key is written only from a function. The admin path checks a token held in an environment variable, so the token never enters the code.
To create the token first, run this command with a value of your own. A function reads a variable’s new value only after it is deployed again, so the variable exists before the function does:
The command prints the UUID of the variable:
Create a function named tenant-app with this code, and instantiate it on the application. For the steps in each interface, refer to Functions quickstart, which creates a function and instantiates it on an application.
The tenant ID must be an integer, because a query refuses a JavaScript string as a parameter. The lookup key is the hostname, which the request always carries, because KV Store has no operation that lists keys.
Then add the rule that runs the instance on every path, as Run a function on an application describes, with these values:
- Name:
tenants - run tenant-app. - Phase: Request Phase.
- Criterion:
${uri}starts with/, so every path of every tenant hostname runs the function. - Behavior: Run Function with the
tenant-appinstance.
In the API, the body of POST /v4/workspace/applications/<application-id>/request_rules carries the ID of the tenant-app instance in attributes.value:
Every request to the workload runs tenant-app. A new rule takes a few minutes to reach every data center.
Configure tenant onboarding
Onboarding a tenant is four changes, and none of them is a deployment: the tenant’s rows, its configuration key, its hostname on the workload, and its DNS record. This section onboards acme.app.example.com with the tenant ID 1.
Replace <database-id> with the ID of saas-data. The first statement creates the table on the first onboarding and does nothing afterward:
The API answers 200 with one entry per statement. Read data[].error on each entry, because a failed statement also returns 200.
Send the configuration to the admin path of the tenant application, on the workload domain, with your token:
The function answers 201 with Tenant stored. A request without the token answers 401.
Send every hostname the workload answers, the new one included, because domains replaces the list:
The API accepts the update, and the workload lists the hostname in domains.
In the example.com zone of Edge DNS, add a CNAME record named acme.app whose value is the workload domain. For the steps, refer to Add, edit, or delete a record.
The tenant answers on acme.app.example.com once the workload change propagates, which takes several minutes. KV Store makes a new key visible everywhere within 60 seconds, so a tenant can answer 404 in some locations during its first minute.
Verify the setup
-
A tenant sees its own data. Request each tenant hostname:
The body names the tenant and holds only its rows:
The same request to
globex.app.example.comnamesGlobexand holds none of Acme’s projects. -
An unknown hostname gets nothing. Request the workload domain, which has no tenant key:
The command prints
404. -
The admin path refuses a request without the token. Send the
PUTfrom Configure tenant onboarding without theAuthorizationheader. The function answers401. -
The certificate covers the tenant. Send
GET https://api.azion.com/v4/workspace/tls/certificates/<certificate-id>. The certificate readsactive, because the workload uses it, and an HTTPS request to each tenant hostname completes its handshake.
A tenant that answers Azion’s placeholder 404 page instead of Unknown tenant is still waiting for the workload change to propagate. When the function does not answer at all, turn on Debug Rules to see which rules ran.
Measuring results
| Metric | Where to read it | What working looks like |
|---|---|---|
| Time to onboard a tenant with its hostname | The time from the first onboarding call to the first answer from the tenant hostname that names the tenant | Bound by the workload propagation of the hostname change, with no deployment in the path |
| Cross-tenant data exposure | A request to each tenant hostname, run on a schedule, compared with the tenant the hostname belongs to | Every response names its own tenant, and no tenant ID appears under another hostname |
| Response time per tenant | The requestTime and requests of workloadMetrics grouped by host. Refer to Real-Time Metrics GraphQL fields | Close across tenants, since every tenant runs the same function |
Best practices
- Take the tenant ID from KV Store, never from the request. The hostname selects the key, and the key holds the ID that every query binds. A tenant ID read from a header, a cookie, or a path segment lets a visitor ask for another tenant’s rows.
- Keep the tenant key derivable from the request. No interface lists the keys of a namespace, so
tenant:<hostname>is the only way back to a tenant’s configuration. For the convention, refer to Derive a key name from what the request already carries. - Cache by the full request, never by the path alone. The default cache key carries the host, and a function that caches through the Cache API keys the entry by the request, which carries the hostname too. A key built from the path alone would hand one tenant’s page to another. For the key format, refer to Cache keys.
- Name a namespace once. A namespace cannot be renamed or deleted, and names are case-sensitive. For the naming rule, refer to Name a namespace as though you can never change it.
- Rotate the admin token by deploying the function again. A function reads a changed variable only after it is redeployed, so a rotation is a variable change followed by a deploy.