Veefore docs

Tenant isolation

Two independent layers, either of which would be sufficient — and why that is the point.

The organization is the tenant boundary. Every resource in Veefore is reachable only through the organization that owns it.

Two layers enforce it, independently.

#Layer 1 — the repository scope guard

Every query in the data layer requires a tenant scope. Not "should have" — requires. A query without one is refused before it reaches the database.

ts
// Every repository method takes a scope as its first argument.
await ctx.repos.agents.findById({ organizationId: actor.organizationId }, agentId);

Passing an id from another organization returns nothing, and a request that attempts it is refused with TENANT_MISMATCH.

`TENANT_MISMATCH`, not `404`

A cross-tenant attempt is reported as a tenant mismatch rather than as a missing resource, so it is distinguishable in the audit trail from a typo.

That distinction is the difference between noticing enumeration and not. A 404 looks like someone mistyped an id; a stream of TENANT_MISMATCH looks like someone is iterating.

Crossing the boundary requires an explicit, named justification in code. Background jobs use it — a job queue is inherently cross-tenant — and request handlers do not.

A real bug this caused

Every background job claim hit the scope guard, because claiming a job means reading across tenants. The error was swallowed by the poll loop, and 67 jobs wedged every queue permanently with no alert.

The fix was to make the cross-tenant intent explicit at the call site rather than to weaken the guard. Claiming now carries a justification, abandoned jobs are reclaimed and dead-lettered, and overlapping poll ticks are prevented.

Worth knowing because it illustrates the trade: a guard strict enough to catch real mistakes will also catch legitimate exceptions, and the right response is to name the exception rather than to loosen the rule.

#Layer 2 — PostgreSQL row-level security

ini
DATABASE_ENABLE_RLS=true

Migration 0009_row_level_security.ts applies RLS policies to every tenant-scoped table. With it enabled, the database itself refuses to return rows outside the current tenant context, regardless of what the query asked for.

Enable it in production

Layer 1 already requires a tenant scope on every query and refuses without one. RLS exists so that a bug in layer 1 — a query that forgets its scope, a new repository method that does not inherit the guard, a raw query someone added in a hurry — does not become a cross-tenant data leak.

Two independent layers, each sufficient on its own. The point of defence in depth is that you do not have to be confident the first layer is perfect.

#The hierarchy

text
Organization                 ← the isolation boundary
└── Workspace                ← scoping, not isolation
    └── Environment          ← scoping, not isolation

Workspaces and environments divide resources within a tenant. They are useful for scoped role grants and for separating deployment tiers, and they are not a security boundary between tenants — because they are inside one.

One organization per tenant

Do not use organizations to separate your own teams. It fragments members, roles, SSO and the audit trail across boundaries that were designed to be impermeable, and you end up fighting the isolation you asked for.

Use workspaces for teams. → Tenancy

#Scope headers

http
GET /v1/agents
Authorization: Bearer aos_sk_…
x-veefore-workspace: wsp_01JAV…
x-veefore-environment: env_01JAV…

Headers select a scope within your organization. They cannot select another organization — the tenant comes from the authenticated session, never from a header.

Supplying a workspace or environment you have no access to is refused. These are a filter over what you can already see, not a way to see more.

#Agents are bound to an environment

An agent belongs to exactly one environment. A staging identity cannot act in production — not because a policy forbids it, but because the agent is not there.

Structural beats configured

There is no rule to misconfigure and no setting to get wrong. The agent simply does not exist in the other environment.

That instinct runs through the design: prefer a guarantee that is structural over one that is configured, because configuration drifts and structure does not.

#Cross-tenant A2A

A2A is the one place agents from different organizations interact, and it is built so that interaction does not weaken isolation.

  • A connection must be explicitly registered, with the remote party's public key.
  • It starts PENDING and stays unusable until the remote passport verifies against that key.
  • Every invocation runs the full twelve-stage pipeline on your side.
  • Blocking is immediate and applies to in-flight sessions.

A verified remote agent gets to invoke the capabilities you registered on the connection, under your policy. It gets no access to your data model, no query surface, and no visibility into anything else in your tenant.

→ A2A

#What is shared across tenants

Almost nothing, and it is worth being precise about the exceptions:

Shared Why it is safe
The platform signing key yes Public half only; signs passports. Knowing it grants nothing
The gateway audience yes Published deliberately. It is the value you sign for
Rate limit buckets no Keyed per tenant
Nonce tracking no Keyed per agent
The job queue table-level Rows are tenant-scoped; claiming carries an explicit justification
Error codes and messages yes Static text, no tenant data

#Verifying it

bash
npm run test:security        # cross-tenant access cases
npm run test:integration     # repository scope guard behaviour
npm run smoke                # signup through enforcement, two organizations

The security suite includes cases that attempt to read, update and act on another organization's resources through every surface that accepts an id.

#Operational checklist

  • [ ] DATABASE_ENABLE_RLS=true in production
  • [ ] One organization per tenant
  • [ ] Role grants scoped to a workspace where the team only needs one
  • [ ] TENANT_MISMATCH in the audit trail reviewed, not ignored
  • [ ] A2A connections registered with an out-of-band key, and re-verified on a schedule

Identity, authority, policy and enforcement for autonomous AI agents.