Platform · Access control
Access control, without the hand-built checks.
Declare who can touch what once, and it holds on every read, write, and search you add later. Leave the boundary off a call, and it's rejected outright, not silently widened.
The alternative
What you'd otherwise build.
Every platform lets you check a permission. Few make the check itself something you declare once instead of writing by hand, everywhere.
- Enforce it again for every new kind of caller. Your app, an internal script, and an agent over MCP all need the same rule enforced. Hand-built means three separate implementations to keep in sync, not one, and they drift.
- Hand-write every permission check. A check wherever your code touches data, the actual enforcement, not just a note in a design doc. Multiply by every new caller you add.
- Reinvent the same shape for every new user. Without a reusable role, one org for one user is a copy-pasted rule, not a declaration. A hundred users is a hundred near-identical rules to keep in sync, and the one you forget is the one that's wrong.
- Filter after the fact, and hope. Enforcement bolted on as a post-query filter is one missed clause away from a real leak, not a bug report.
- Build an audit trail, including who read something, from scratch. Most systems log writes. Almost none log reads, and when an auditor asks who saw something, "we don't know" is not an answer.
What's actually different
Namespaces model your own ownership axes.
Design your own ownership axes however your product needs them, team, region, tenant, org, client, whatever fits. Every one registers the same one-time way; none of them is special-cased.
// Register your own ownership axis. entityBacked makes "team:<id>" an
// existence-checked reference, not just a free-form label.
await client.identity.registerNamespace({
body: { namespace: 'team', entityBacked: true, specificityRank: 10 },
});
// Create an entity in it, exactly like org or client.
const team = await client.identity.createEntity({
namespace: 'team',
body: { externalId: 'team-eng-platform', name: 'Platform Engineering' },
});From here, a scope:team value anywhere on the platform, a record's ownership, a token's data scope, a role's clause, must resolve to a real team entity or the write is rejected. The same guarantee client and org get once registered.
The reusable part
Define it once. Bind everyone who needs it.
A role is a permission shape, not a person. Define it once and bind as many principals to it as you need.
// Define once, in this app context:
const role = await client.auth.createRole({
contextId: 'clinic-intake',
body: {
roleId: 'case-handler',
name: 'Case handler',
scopes: [{
allowed_actions: ['records:r', 'records:c', 'search:r'],
data_scope: { 'scope:org': ['${{ self.scope.org }}'] },
}],
},
});
// Bind as many principals as you want. Each one is automatically confined
// to their own org, without minting a new scope per person.
await client.auth.createAccessProfile({
contextId: 'clinic-intake',
body: { principalId: 'usr_alice', roleId: 'case-handler' },
});
await client.auth.createAccessProfile({
contextId: 'clinic-intake',
body: { principalId: 'usr_bob', roleId: 'case-handler' },
});The ${{ self.scope.org }} placeholder resolves to each principal's own value at request time, not a value you look up and hardcode per person. A hundred case handlers reuse this same role definition; a hundred-and-first costs one more binding, not one more rule to author.
Enforced, not requested
Every call must say where it's looking, or it's refused.
A credential scoped to one org can never read, write, or search outside it, no matter what the call itself says. Leave the scope off a list or search call and the request is rejected outright, the error naming the missing dimension, rather than quietly widening to everything the credential could theoretically reach. The same rule governs a create: a write that omits ownership for a dimension the credential carries is refused, not silently written as an orphaned, owner-less row. See the database, search, and document ingestion building blocks for this same mechanic on records, queries, and documents.
The rest of the picture
A credential that can never exceed its profile, and a record of who read what.
Two credential shapes, one enforcement path
A permanent scoped key for an agent or a service, or a short-lived token for a browser. Both inherit a profile and can never exceed it, whichever way they were minted.
Read-access logging, not just writes
Every write to an audited model accrues a tamper-evident version history by default. Read-access logging is real too, opt-in per context and off by default, for a workload that needs to show who saw something, not just who changed it.
self.scope.org placeholder above. And it only governs the Vectros data plane: this is not a standalone authorization service you can point at a database or API outside Vectros.See it in a real app
Every mechanism on this page, exercised in one shipped app.
The RAVV reference app runs on two real roles, not one. hr-admin holds org-wide authority by role alone; case-handler's access resolves per request from a user's own membership records, split into separate clauses for create, read, update, and delete because each genuinely needs a different scope. Both are ordinary bindings on the same org and client namespaces this page describes, enforced identically for every principal the app invites.
This is not a sixth building block sitting next to the other five. It is what already runs underneath every one of them.
See the five building blocks it governs on the platform page.