Skip to content
Payment Emulator LabDocumentationOpen console

Runtime

Identity, roles and scope

Who is asking decides what an answer contains. This is the one rule the controlplane is built on, and it is enforced in the service layer rather than route byroute, so that adding

4 min read

Índice / Index · Español

Who is asking decides what an answer contains. This is the one rule the control plane is built on, and it is enforced in the service layer rather than route by route, so that adding a route cannot be a way of forgetting it.

The provider surfaces are not part of this. An integration authenticates with its application's own credential; nothing below applies to it.

#Two credentials, and they are not the same thing

The operator token A session token
Where from ADMIN_TOKEN POST /auth/login
Acts as admin the person's own role
Is a person No Yes
For Bootstrap, CI, the seed, the console's server before anyone exists Everything a human does

The operator token acts as an administrator because it is how the first administrator comes to exist — but it is nobody, and anything that has to belong to a person refuses it. Registering an application or opening a bank account with it fails with owner_required unless it names whose it is.

There is no default operator token. The API refuses to start without one of at least sixteen characters, because an emulator shipping a well-known password is an emulator with no password.

The first administrator is created through POST /auth/bootstrap, which the operator token opens. Somebody has to exist before anybody can sign in to create anybody. npm run setup makes that call as its last step, and npm run admin:create makes it on its own; both go through the HTTP surface rather than the database, so neither can create an account the API would have refused.

#The three roles

Role Is Sees
admin The operator of this installation Everything
merchant A business Its own applications, its own money, its own accounts
customer A buyer Its own wallet and accounts — in the checkout, not in the console

A person carries two optional keys that tie them to the ledger: walletKey (where their money comes from when they pay with balance) and merchantKey (under what name they are paid). A key that does not match the role is refused, because it is a key nothing would ever read.

#How a route is guarded

RolesGuard resolves the credential, then compares the caller's role against the route's @Roles(...). Absent means administrators only: the control plane's default has to be the closed one.

Method-level @Roles overrides the controller's, which is how the banks catalogue is readable by everyone and writable only by an administrator — choosing where your account is means seeing the list.

#How rows are scoped

Guarding a route says who may call it. It says nothing about which rows come back. That is a single expression per module, written once:

ts
visible(caller): FilterQuery<Entity> {
  if (caller.role === 'admin') return {}
  if (!isPerson(caller)) return {}
  return { owner: caller.user.id }
}

Every listing and every single-row read intersects it. Two consequences worth stating out loud:

  • An id the caller may not see answers 404, never 403. A 403 on a real id confirms the id is real, which turns an error code into a way of enumerating other people's records.
  • A filter can narrow the scope and never widen it. The endpoint's own scope is intersected with the caller's filters, not merged.

#Sessions

A session is a row with a hashed token and an expiry. Presenting an expired one deletes it and answers as if it never existed, which also keeps the table from growing a tail of dead rows nobody sweeps. Changing a password ends every session opened with the old one.

The console never holds a session token in the browser: it is sealed into a cookie by Nitro, and every proxied request is made as the person who signed in. The operator token is the fallback and only that. See the console.

Payment Emulator Lab · RonuSoftwareMIT