API Maker

The framework for AI era

Level 3 · ScaleArchitecture 08 of 13

Multi-tenant: a database per customer

One product and one set of APIs, with the data of every customer in a database of its own.

True multi-tenancy, supported out of the box: each request names its tenant, and API Maker runs it on that customer's own database, found in a tenants table. Customers share the servers, never the data.

See it live

Every request is drawn as it travels. Slow it down, pause, go step by step, or open the full canvas and zoom in.

api-maker/architectures/multi-tenantLive
  • Acme
  • Globex
  • Initech
  • Umbrella
  • Tenant lookup
  • Write
  • HTTPS and WSS up to Caddy
Tenants
3customers
Databases
3one per tenant
API servers
2shared
Pools
6tenant × server

One set of APIs for every customer. Every customer calls the same APIs. The tenant is part of the path: /api/gen/saas/crm::acme/crm/customers.

How it works

The same steps as the diagram, in more detail.

  1. One set of APIs for every customer.

    The product has one set of APIs, schemas and custom code. The instance "crm" is marked as a multi-tenant structure, and each request names its tenant after two colons: crm::acme, crm::globex. API Maker runs the same API on the database of that customer, with the same permissions and hooks for all of them.

  2. Each tenant is a row of a table.

    The instance points to a table of your own, through a secret: which column holds the username of the tenant, which one holds its connection string, and an optional filter such as active tenants only. On the first request of a tenant, API Maker reads its row, decrypts the connection string, opens a pool for that tenant and reuses it for the next requests.

  3. Every customer in a database of its own.

    Isolation does not rely on a tenant_id column that every query must remember: each customer has a separate database, so a request of one tenant is physically unable to read the data of another. Backups, restores, exports and deletions happen per customer.

  4. Each customer where it fits.

    Where a tenant's database lives is only a connection string. Put many small customers on a shared server, give a large one a server of its own, move one when it grows: copy its database, update its row, and call the multiTenantInstanceUpdated system API so every server resets its pool.

  5. A new customer is a new row.

    Onboarding a customer is creating its database, running your migrations on it, and inserting one row in the tenants table. A regulated customer can even keep its database in its own data center, reached through a private tunnel, while using the same product as everyone else.

  6. Servers for everyone, databases per customer.

    API servers are shared by all tenants and scale like any fleet of API Maker: add servers behind Caddy and each one serves every tenant, opening pools on demand. Cached responses are kept per tenant, so one customer never receives the cache of another.

Why choose this architecture

Isolation by design

Each customer has its own database. No tenant_id to forget in a query, no way to read another tenant's data.

One product to maintain

The same APIs, schemas, permissions and custom code serve every customer. Fix once, every tenant gets it.

Each tenant where it fits

Small tenants share a server, large ones get their own, regulated ones keep their database at home.

Onboarding is a row

Create the database, insert one row in the tenants table, and the new customer is served at once.

Encrypted connection strings

Connection strings in the tenants table can be encrypted by API Maker and are decrypted only when a pool opens.

Per-customer operations

Back up, restore, export, move or delete one customer without touching the others.

Server configuration

Plain VPS from any provider, or your own servers, with Ubuntu 22.04 LTS. Sizes are a starting point: measure and adjust.

Caddy load balancer

×1
CPU
2 vCPU
Memory
2 GB
Storage
20 GB SSD

Runs

  • Caddy: HTTPS and WSS for app.example.com
  • Optional: certificates for the domains of your customers

Ports

  • 443 and 80: from the internet

API servers

×2 or more
CPU
4 – 8 vCPU
Memory
8 – 16 GB
Storage
40 GB SSD

Runs

  • API Maker, run by systemd, behind Caddy
  • A connection pool per tenant, opened on demand

Ports

  • 38246 and 38245: from Caddy only

Each server keeps a pool for every active tenant: plan the connection limits of the database servers for it.

Shared tenant DB servers

as needed
CPU
8 vCPU
Memory
32 GB
Storage
500 GB NVMe

Runs

  • One database per small tenant
  • The same engine as the multi-tenant instance

Ports

  • From the API servers only

Dedicated tenant DB servers

1 per large tenant
CPU
16 vCPU
Memory
64 GB
Storage
1 TB+ NVMe

Runs

  • The database of one large customer

Ports

  • From the API servers only

Platform

×1
CPU
4 vCPU
Memory
8 GB
Storage
100 GB NVMe

Runs

  • The tenants table, in one of your databases
  • API Maker DB and Redis
In the secret of the admin user: where the tenants aresecret
{    "multiTenant": {        "crm": {            "instanceName": "catalog",            "databaseName": "public",            "collectionName": "tenants",            "usernameColumn": "username",            "connectionStringColumn": "connection_string",            "find": { "active": true }        }    }}

Then check "Is Multi Tenant Structure Instance" on the instance "crm" and set its "Connection String Multi Tenant" to multiTenant.crm.

The same API, the database of each customerrequests
GET /api/gen/saas/crm::acme/crm/customers      # the database of AcmeGET /api/gen/saas/crm::globex/crm/customers    # the database of Globex# A tenant moved to another server: its row is updated, then every server resets its poolPOST /api/system-api/saas/multi-tenant-instance-updated   { "instanceName": "crm", "username": "globex" }

Custom code calls it as sys.system.multiTenantInstanceUpdated.

Good to know

  • Every schema change must run on every tenant database: automate your migrations for all of them.
  • Pools add up: API servers × active tenants. Watch the connection limits of the database servers.
  • All tenant databases of a multi-tenant instance use the same database engine.
When you outgrow itThe data tier itself must grow beyond single servers. Clustered Redis and databases

Questions

How does a request choose its tenant?

The tenant is written after the instance name, separated by two colons: /api/gen/saas/crm::acme/…, or sent in the x-am-tenant-username header. Your frontend takes it from the account the user belongs to, and a user token made for a tenant works for that tenant only.

Where are the connection strings stored?

In a table of your own, the tenants table, with a column for the username of the tenant and one for its connection string. Turn on the encryption conversion of that column and API Maker stores it encrypted.

Can each customer use a domain of its own?

Yes, with the on-demand TLS of Caddy: it gets a certificate for a domain the first time the users of a customer reach it. Its ask setting points to an endpoint that answers 200 for the domains you allow, for example a custom API that looks the domain up in the tenants table.

Can tenants use different database types?

Not within one multi-tenant instance: its tenants share its engine, for example PostgreSQL. A product can have several multi-tenant instances, each with its own engine and tenants table.