InventDB
All articles Security

Sign-in, roles and grants for people, apps and agents

An InventDB instance is its own authorization server. People sign in with a password, AI agents and apps sign in through OAuth 2.1 with PKCE, and every request then runs as one caller whose role and grants decide what it may read and change.

Every request to a database has to settle two questions before it touches any data: who is calling, and what are they allowed to do. In InventDB both answers are produced inside the instance, by the same server that runs the query, with no separate identity service in front of it.

This article follows a request from sign-in to the grant check. It covers how a person signs in with a password, how an AI agent connects over OAuth 2.1 without anyone pasting a credential into its settings, how roles and grants are stored and combined, and the trade-offs that come with each choice.

Signing in with a password

A person signs in by posting a username and password:

POST /api/auth/login
Content-Type: application/json

{ "username": "ann", "password": "<password>" }

The server finds the person, checks the password against its stored bcrypt hash, and returns a JSON Web Token. The token is signed with HMAC-SHA256 (HS256). It carries the person's record id as its subject, their role, the time it was issued, an issuer and an audience, and an expiry eight hours after issue. The subject is the record id rather than the username, so renaming a person does not change whom their grants and audit entries point to.

A wrong username and a wrong password get the same answer, so the login form cannot be used to find out which usernames exist. An account that an administrator has deactivated is refused with a 403 even when the password is right. Passwords are stored only as bcrypt hashes at cost factor 12, each with its own salt, and changing a password requires the current one.

Every request runs as one caller

A client sends its token with each request, in the Authorization header:

GET /db/pms/leases/L-1042
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

One authentication layer sits in front of the routes. It checks the token's signature, issuer, audience and expiry. If the token is not a JWT, the layer looks it up as an OAuth access token instead. Either way the result is the same three facts about the caller: who they are, their role, and the app they signed in through, if there is one. The layer attaches those facts to the request before any handler runs, and the database engine reads them at its read and write entry points. That is how the row rules described in our row-level security article reach every query without any handler having to pass them along.

Checking a token is constant work: one HMAC over a JWT, or one indexed lookup for an OAuth token. It does not grow with the number of people, roles or grants on the instance.

Password sign-in and OAuth sign-in both end in one caller, which passes the grant check and then the engine's row rules A person password, then a JWT An AI agent or app OAuth 2.1 with PKCE Authentication checks the token attaches who, role, app INSIDE THE INSTANCE Grant check namespace or type, at read, write, delete, admin Engine row rules on every read and on every write
Both ways of signing in end in the same caller. The grant check and the engine's row rules see who, which role and which app, and never need to know how the caller signed in.

Roles

A role is a named set of grants. Every person has a role on their account, and administrators can attach further named roles to them. A few roles are built in:

RoleWhat it allows
superadminEverything. Only a superadmin can make another person a superadmin.
adminFull access to every namespace, plus managing people, roles, grants and row rules.
analystNo business data of its own: an analyst sees the namespaces granted to them personally. In InventDB SOAR, analysts also create and manage their own reports.
readerThe default for a new person. It carries no grants of its own, so a new person sees no business data until an administrator grants some.

Administrators create their own roles with names of 1 to 64 letters, digits, underscores, hyphens and colons, apart from a few reserved names. A role can be attached to one person, or to many people in one call, either by a list of ids or by a SQL condition over the list of users.

Roles per app. A person can hold a different role in each app they use. When an app signs a person in through OAuth, the token records which app it is, and for requests made with that token the person's role in that app, if they have one, replaces their account role. Ann can be a manager in a property app and a viewer in a reporting tool on the same instance with one account. Administrators keep their administrator role in every app.

Grants

A grant has three parts: a scope, a resource and an access level. The scope is either a namespace, which covers every type inside it, or a single type such as pms.leases. The access level is one of four, and each level includes the ones before it:

LevelAllowsRequests that need it
readReading records and running queriesGET requests, search and filter calls, and a SQL SELECT on every table it names
writeRead, plus creating and updating recordsPOST, PUT and PATCH on records
deleteWrite, plus deleting recordsDELETE requests and bulk-delete calls
adminEvery level on that resourceAnything the levels above allow

A person's grants come from two sources: grants given to them directly, and the grants of every role they hold. The check tries direct grants first, because that is a single indexed read, and then the roles. Any grant that covers the request at the required level allows it; if none does, the request is refused with a 403 and a message naming the missing level and type, such as "You don't have write access to pms.leases". Namespace and type names are matched without regard to case, because the engine treats those names case-insensitively.

Any grant can also carry a row rule that narrows which rows it covers, such as agent = :current_username. When a person holds several grants on one type, any one of them allowing a row is enough. The engine's own system namespaces, which hold users, roles and settings, accept writes only from administrators; people and roles are changed through dedicated endpoints that validate each change before storing it.

AI agents and apps: OAuth 2.1

An AI agent should not need someone to paste a password or a long-lived key into its settings. For MCP clients, the instance acts as its own OAuth 2.1 authorization server. A client that knows only the instance's address goes through these steps:

  1. Discovery. A call to /mcp without a token gets a 401 whose WWW-Authenticate header names the instance's protected-resource metadata. From there the client reads the authorization server metadata, which lists the endpoints and the methods the instance supports.
  2. Registration. The client registers itself with Dynamic Client Registration (RFC 7591), sending its name and redirect URIs, and receives a client id.
  3. Authorization with PKCE. The client opens the person's browser at the authorize endpoint with a code challenge. PKCE with the S256 method is mandatory, and a request using the plain method is refused. The redirect URI must match one the client registered.
  4. Sign-in and consent. The person signs in on the instance's own login page, and a consent page names the client that is asking. Consent is remembered for 90 days, so a returning client skips the prompt.
  5. Code exchange. The instance redirects back with an authorization code that is valid for 60 seconds and works once. The client exchanges it, together with its code verifier, for an access token valid for eight hours and a refresh token valid for 30 days.
The OAuth 2.1 sequence between an MCP client and an InventDB instance, from discovery to the first authorized tool call MCP client InventDB instance POST /mcp without a token 401, naming the metadata GET the authorization server metadata endpoints; PKCE with S256 only register (RFC 7591) client id browser: authorize, with an S256 challenge person signs in and consents; code valid 60 s exchange the code with the verifier access token 8 h, refresh token 30 days POST /mcp with the token: tools run as the person
The client starts with nothing but the instance's address. Every step after the first 401 is driven by metadata the instance publishes, and the person types their password only into the instance's own page.

OAuth access tokens are opaque random strings rather than JWTs, and the instance looks each one up on every request. Revoking a token through the revocation endpoint (RFC 7009) therefore takes effect on the client's next call. A refresh reads the person's role for that app again, so a role change reaches an agent at its next refresh without a new sign-in.

For software that runs with nobody at the keyboard, such as a service that reads the change feed, an administrator creates a service account. Its client uses the client-credentials grant to get an access token bound to that account and its role, without a refresh token. A client that registered itself cannot use that grant.

Once connected, an agent is a caller like any other. Every MCP tool it calls is checked against the person's role and grants, and the SQL it runs and the changes it makes are held to the person's row rules, as our article on the MCP server describes.

Trade-offs to know

  • A password sign-in token is self-contained. It is valid for eight hours, and the role inside it is the account role the person had when they signed in. Grants, attached roles and row rules are read on every request, so changes to them apply on the next call. A change to the account role itself applies when the person next signs in. OAuth access tokens are looked up on every request, so revoking one applies at once.
  • No outside identity provider. The instance is its own authorization server. People sign in with an InventDB username and password; the instance does not hand sign-in off to a company directory or a social login.
  • Administrators are not limited by grants. The admin and superadmin roles pass every grant check and are not filtered by row rules, by design, because they are the people who write the rules. Keep those roles for the few people who need them.
  • bcrypt reads 72 bytes. Characters past the 72nd byte of a password do not change its hash, a property of the algorithm.

Putting it together

Here is a role for leasing agents: read access to the whole pms namespace, and write access to leases limited to the leases the agent handles.

POST /api/roles
Authorization: Bearer <admin token>
Content-Type: application/json

{
  "name": "leasing:agent",
  "description": "Leasing agents",
  "grants": [
    { "scope": "namespace", "resource": "pms", "accessLevel": "read" },
    { "scope": "type", "resource": "pms.leases", "accessLevel": "write",
      "rowFilter": "agent = :current_username" }
  ]
}

Attaching it to Ann is one more call, and it applies to her next request:

POST /api/users/ann/roles
Authorization: Bearer <admin token>
Content-Type: application/json

{ "role": "leasing:agent" }

The same model runs on InventDB Serverless and InventDB SOAR, because both run on the same engine. In InventDB SOAR, people, roles and grants are also managed under Team & Access. To connect an AI agent, give its MCP client the instance's /mcp address: the client performs the discovery, registration and PKCE steps above, and the person signs in once in their browser.