Reference

Multi-tenancy

Every Aaly project is multi-tenant capable the moment it's created. Tenant scoping isn't a filter you write into a query — it's applied by the platform on every request, for every entity, without an opt-in.

The guarantee

A request carrying one tenant's token cannot read, list, update, or delete another tenant's record. A cross-tenant read on a record by ID returns 403, not a filtered-out 404 — the platform knows the record exists and is refusing access to it, which is the property that makes "there is no query you can write that returns another tenant's row" true rather than aspirational.

The two project-level flags

Set on the project (create_project / update_project via MCP, or in the dashboard):

FlagControls
publicSignupWhether the public can self-register a new tenant via /auth/signup at all. Default false — a new project starts locked down.
multiTenantOnce publicSignup is true: whether every signup without a tenantId creates its own separate tenant (true, the multi-org SaaS case), or only the very first signup bootstraps one shared tenant and everyone after it must join with that tenantId (false, the default). Has no effect while publicSignup is false.
{ "multiTenant": true, "publicSignup": true }

How a tenant gets created

  • publicSignup: false (a new project's default) — end users cannot create a new tenant via /auth/signup at all. You provision the first (and any later) tenant yourself via POST /tenants, which also creates that tenant's first user. This is the locked-down starting state, on purpose.
  • publicSignup: true, multiTenant: false — the first sign-up without a tenantId bootstraps the project's one shared tenant; every sign-up after that must include that tenant's tenantId to join it.
  • publicSignup: true, multiTenant: true — every sign-up without a tenantId creates its own new, separate tenant.
  • A sign-up with an existing tenantId joins that tenant and shares its data with every other member of it, regardless of either flag above.

Each tenant also carries its own publicSignup flag (default true, independent of the project-level one) — so you can lock one specific tenant down to owner-provisioned users only, without affecting any other tenant on the same project.

Full sign-up/sign-in request and response shapes are on Auth.

Why this matters more than it sounds like it should

The most common failure mode in an AI-generated backend isn't the first version — it's the fifth change, when a tenant filter that was correct in the original code quietly goes missing from one new route. Because scoping here is enforced by the platform reading the request's token, not by a query a person or a model wrote, there's no line of application code where that filter could be forgotten in the first place.