VendoVendo Docs
Concepts

Multi-tenant

How Vendo isolates tenants at the database, network, and binding layers.

Vendo is a multi-tenant platform: many independent tenants run their own copies of the same tool side-by-side on the same infrastructure. This page covers the boundaries Vendo enforces so a tool author can rely on them.

The short version: a tenant's connections, deployments, and credentials cannot leak across tenants — that's enforced at the Postgres layer, at the proxy, and at the DNS layer.

Boundary 1 — Database (RLS)

The core tenant-scoped tables (tenants, deployments, deploy_logs, connections, app_connection_bindings) all have Row-Level Security enabled. Each policy reads the caller's tenant id from the JWT app_metadata claim and rejects any row whose tenant_id doesn't match.

The only role that can cross the boundary is service_role, used by Vendo's own API routes after they've already verified ownership via the user-context RLS-gated read. Tool code runs through the SDK against the proxy and the deployments API — it never speaks Postgres directly, so RLS is the floor.

For app/binding pairs there's an additional belt-and-braces check: the enforce_binding_tenant_match trigger on app_connection_bindings raises if the bound app's tenant id doesn't equal the bound connection's tenant id. Even a service_role insert with mismatched ids gets refused.

Boundary 2 — Connections are per-tenant

A connection (the row that holds a tenant's credential for one integration) is owned by exactly one tenant via connections.tenant_id with ON DELETE CASCADE to tenants(id). Deleting a tenant wipes their connections; no orphan rows.

The follow-on rule: bindings (app_connection_bindings) glue a connection to an app. The trigger above forbids a binding from spanning two tenants. So even if a tool author accidentally tries to bind connection X to app Y across the tenant line, Postgres refuses.

Boundary 3 — Subdomain isolation

Every running deployment gets its own subdomain on vendo.run. Two formats coexist:

  • Single-level (the format for all new deployments): {tenant_slug}-{deployment_slug}.vendo.run. The two slugs collapse into one DNS label, covered by free Universal SSL on *.vendo.run.
  • Two-level (legacy): {deployment_slug}.{tenant_slug}.vendo.run. Existing deployments keep this format on rename via selectFormatForRename so customer-embedded URLs don't silently break.

A partial unique index on deployments.subdomain (where status != 'destroyed') prevents collisions across tenants. A (tenant_a, deploy_z) rename that would render to an already-taken {tenant}-{deploy} surfaces a 409 from /api/deploy instead of stealing traffic.

The app-proxy at the edge routes by Host, so requests for tenant-a-prod.vendo.run only reach tenant A's container — even if tenant B has a deployment named prod.

Boundary 4 — Proxy authentication

Each deployment gets a vendo_sk_* bearer that is scoped to one (tenant, app) pair. The proxy hashes the bearer, looks it up in KV, and reads tenantId and appId from the lookup result. From that point on every binding resolution, every spend-cap check, and every usage write happens under that single tenantId.

If a tool somehow leaked its vendo_sk_* to another tenant, the leaked key would still only be able to consume the origin tenant's balance and reach the origin tenant's bound connections — never the leak recipient's. The blast radius is bounded by tenant.

What this means for tool authors

You do not need to write tenant checks in your tool. The SDK reads the deployment's bound connections from /api/deployments/me/connections, and that endpoint already filters by the calling app key's tenant. Any provider token you get back is, by construction, owned by the same tenant as the deployment you're running in.

The one place you should be aware of cross-tenant rules is if you build a tool that runs OAuth flows on behalf of end users (i.e. forRequest / forUser — see Request scoping). User-scoped tokens are still tenant-scoped: a user identified by X-Vendo-User-JWT can only resolve to connections inside the same tenant as the calling app key.

RLS, the binding-tenant trigger, the subdomain unique index, and the proxy KV lookup are independent layers — any one of them on its own would prevent cross-tenant access. Together they form Vendo's "defence in depth" tenant boundary.

On this page