Actors, access and identity
| Concept | Rule |
|---|---|
| Actor | { id, roles }, passed as runtime.dispatch(cmd, { actor }) (default null, anonymous). Never read from the payload: a payload field named actor is stripped. decide gets it as ctx.actor. |
| SYSTEM | Reactions and sent commands run as SYSTEM ({ id: 'system', roles: ['system'] }). Pass the exported SYSTEM object itself to dispatch as the system from the edge (a subscriber writing back); a structural copy does not count. |
| Reserved | An external actor claiming the id or role system, or whose roles is not an array, is forbidden (params { actor: 'reserved' }) on every command and query before access is read. |
| Access | Required on every command (ATLAS014): 'public' (anyone), 'user' (any actor), 'internal' (only sent by another capability), or ['role', ...] (any of these roles). Checked before reads and decide. Only public commands see actor: Actor | null. |
| Internal | 'internal' commands are never dispatched, not even as SYSTEM: capabilities send them. |
| Membership, ownership | Not in the actor. Check them in decide against records you read (order.customerId !== actor.id). |
| Query access | Required on every query (ATLAS021): 'public', 'user' or roles, never 'internal'. |
Default to 'user'; use 'public' only for anonymous use. For “my record”, key the read by the actor: key: (input, { actor }) => actor.id; never add a user id to the input.
Identity at the edge
Section titled “Identity at the edge”Atlas never authenticates. An adapter resolves the actor. With Express, identify(req) is the identity port:
import { atlasRouter, unauthenticated } from '@ts-atlas/core/express';
app.use(atlasRouter(runtime, { identify: async (req) => { const token = req.header('authorization'); if (!token) return null; // anonymous const session = await sessions.verify(token); // your auth if (!session) throw unauthenticated(); // bad credentials: 401 return { id: session.userId, roles: session.roles }; },}));Since 0.5.0, any throw other than unauthenticated() is an outage (503 store-failed, reported to onError), so a client never drops a valid token because the session store failed. identifyErrors: 'unauthenticated' restores the 0.4 answer while migrating. See the Express guide.