Agentora TechnologiesAgentora
Multi-tenancy

Provisioning a hosted tenant per client — schema and all

Generating a website for a client with no digital presence is the easy half. Serving it at its own address, with its own data isolated from every other tenant, provisioned without a manual database step, is the half that decides the architecture.

Agentora EngineeringJuly 16, 20267 min read

What this system does

Agentora's greenfield track — internally "Digitize from Zero" — serves clients who have no meaningful digital presence at all. It builds a business profile, generates a website from it, then provisions hosting so the site is actually live. This case study covers that last step: how a tenant gets created and isolated.

The problem

A generated site is worth nothing until it is served: at its own address, with its own content, isolated from every other client on the platform. Provisioning is where a multi-tenant product either gets its isolation model right or spends years regretting it.

The default approach is a shared set of tables with a tenant identifier column on every row. It is the fastest thing to build, and its isolation is only as good as every query ever written against it. One forgotten filter in one endpoint — including endpoints written months later by someone who never saw this decision — exposes one client's content to another. The failure is silent, and for a platform serving regulated businesses it is close to unrecoverable reputationally.

The opposite extreme, a separate database per tenant, gives isolation that does not depend on application discipline. It is also slow to provision, multiplies connection and migration overhead, and turns onboarding a small client into an infrastructure event.

There was a second requirement that shaped the design: provisioning had to be fully automated. If creating a tenant needs a human to run DDL, then onboarding does not scale and every new client is a scheduling problem.

Constraints

The boundaries the design had to respect, before any solution was chosen.

  • Isolation must not depend on every future query remembering a filter.
  • Provisioning must be automated end to end — no manual database step per client.
  • The site must be reachable at its own subdomain.
  • Provisioning is deterministic and must not consume AI spend, unlike the generation steps around it.
  • The provisioning job has to be enqueueable while the engine is scaled to zero.

The architecture

The design separates the hosting control plane from tenant data, and uses the database's own namespacing as the isolation boundary rather than a column that application code must respect.

  1. 1

    A shared hosting control plane

    A hosted_tenants table is the control plane: it records the subdomain, the schema name, provisioning status, and which generated site artifact the tenant is serving. This is deliberately shared — it is platform metadata about tenants, not tenant content.

  2. 2

    Each tenant's data in its own Postgres schema

    Tenant content lives in a dedicated schema rather than in shared tables with a tenant column. Isolation becomes a property of which schema a connection is pointed at, not of whether a developer remembered a WHERE clause. A query cannot accidentally cross tenants because the other tenant's tables are not in scope.

  3. 3

    Provisioning runs on the engine, because DDL belongs to the database's owner

    PROVISION_HOST is implemented as a capability on the delivery engine — not because it needs a language model, but because it performs schema DDL, and that belongs in the service that owns the database rather than in a web request handler.

  4. 4

    A deterministic job in an AI job pipeline

    Provisioning makes no model call and costs no AI spend, but it still runs through the same asynchronous capability/job pipeline as the generation steps. One mechanism handles all out-of-band work, so there is no second execution path to secure and monitor. The reason a zero-cost job runs on the AI engine is documented at the call site so the intent is not lost later.

  5. 5

    Enqueue directly, then wake the engine

    Because the engine may be scaled to zero when the request arrives, the API writes the job row directly and fires the scale-up event, rather than asking the engine to enqueue its own work. A service at zero tasks cannot accept a request telling it to start working.

Key decisions and their trade-offs

Every decision below cost something. The trade-off is stated alongside the reasoning.

Schema-per-tenant instead of a shared table with a tenant column

Why
It moves isolation from a convention that every query must follow to a boundary the database enforces. The most likely long-term failure of the shared-column model is a single missing filter in code nobody is reviewing for that concern — schema separation removes that failure mode rather than guarding against it.

Trade-off
Schema count grows with the tenant count, and every migration has to be applied per schema rather than once. That is real operational work that the shared-table model does not have.

Schema-per-tenant instead of database-per-tenant

Why
A schema is cheap and fast to create, which keeps provisioning automated and near-instant, while still giving a hard namespace boundary. Database-per-tenant would buy stronger resource isolation at a cost that does not fit onboarding a small business.

Trade-off
Tenants share the instance, so they share connection limits and resources. A noisy tenant is not fully insulated from the others.

Keep the control plane separate from tenant data

Why
Platform questions — which subdomains exist, what is provisioned, which artifact is live — need to be answerable in one query across all tenants. Putting that metadata inside per-tenant schemas would make routine operations a fan-out.

Trade-off
There are now two places that describe a tenant, and they have to stay consistent with each other.

Run deterministic DDL through the AI job pipeline

Why
It keeps a single asynchronous execution path — one queue, one worker model, one place where auth and retries are handled — rather than adding a second mechanism for non-AI work.

Trade-off
It reads as odd that a job with no model call and no AI cost runs on something called the AI engine, which is why the reason is written down in the code rather than left to be rediscovered.

Technologies used

Data & isolation

  • PostgreSQL schemas
  • Schema DDL automation
  • Supabase

Engine

  • Java
  • Spring Boot
  • Capability job pipeline

Application & hosting

  • Next.js
  • AWS ECS Fargate
  • Subdomain routing

Outcome

  • The greenfield flow works end to end: business profile, then site generation, then host provisioning, with the tenant served on its own subdomain.
  • Tenant isolation is enforced by schema separation rather than by query discipline, so a missing filter in future application code cannot cross a tenant boundary.
  • Provisioning is automated and consumes no AI spend, despite running through the same job pipeline as the generation capabilities.

Known limitations

What this design does not do. Stated because an architecture without documented trade-offs has usually not been examined closely enough.

  • Addressing is subdomain-only. Wildcard DNS is the enabling dependency and is an operational prerequisite rather than something the application controls.
  • Migrations must be applied per tenant schema. This grows with tenant count and needs tooling before the count gets large.
  • Tenants share the database instance, so resource isolation is weaker than database-per-tenant.
  • Single region. Tenant residency in a specific geography has not been designed.

Want this level of rigour on your AI initiative?

Start with a free AI Readiness Assessment, or book a Discovery Workshop to get a scored, costed roadmap.