Personas
A persona is a named bundle of roles, each linked at a relationship scope. Assigning a persona adds the user to each linked role at that scope; the memberships are ordinary rows in lt_user_roles (the single source of authorization truth), and the persona records why they exist. See Identity for provenance, the highest-allowance union and overlay-on-reassign.
All endpoints require authentication plus role-management access (admin type, superadmin, or the engineer role), the same audience that manages users and roles.
Relationships
Section titled “Relationships”Relationship values map onto the member scope lattice (see Work-surface scope):
| relationship | read_scope | write_scope |
|---|---|---|
write-all |
all |
all |
write-self |
all |
self |
read-all |
all |
none |
write-none is accepted anywhere as a synonym for read-all.
The recompute object in mutation responses counts the membership changes: granted (memberships inserted), refreshed (persona-sustained rows overlaid), raised (direct rows lifted to the union) and removed (sustained rows no held persona still grants).
List personas
Section titled “List personas”GET /api/personasReturns every persona with its role links and assignee count.
Example request
curl https://lt.acme.dev/api/personas \ -H "Authorization: Bearer $LT_TOKEN"Response 200
{ "personas": [ { "id": "a1b2c3d4-...", "key": "production-manager", "title": "Production Manager", "description": "Runs the line: works intake and review, watches packing.", "roles": [ { "role": "intake", "relationship": "write-all" }, { "role": "review", "relationship": "write-all" }, { "role": "packing", "relationship": "read-all" } ], "user_count": 2, "created_at": "2026-07-01T08:00:00.000Z", "updated_at": "2026-07-01T08:00:00.000Z" } ]}Get a persona
Section titled “Get a persona”GET /api/personas/:keyReturns one persona and the users who hold it.
Path parameters
| Parameter | Description |
|---|---|
key |
Persona key |
Example request
curl https://lt.acme.dev/api/personas/production-manager \ -H "Authorization: Bearer $LT_TOKEN"Response 200
The persona record plus assignees: [{ id, external_id, display_name, email, assigned_at }].
Response 404
The key is unknown.
Create a persona
Section titled “Create a persona”POST /api/personasCreates a persona with no role links. Link roles with PUT /api/personas/:key/roles/:role.
Request body
| Field | Type | Description |
|---|---|---|
key |
string |
Required. Stable identifier in the role-name alphabet (^[a-z][a-z0-9_-]*$) |
title |
string |
Display title |
description |
string |
The day-in-the-life, one paragraph |
Example request
curl -X POST https://lt.acme.dev/api/personas \ -H "Authorization: Bearer $LT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "key": "production-manager", "title": "Production Manager", "description": "Runs the line: works intake and review, watches packing." }'Response 201
The created persona record.
Response 409
The key already exists.
Update a persona
Section titled “Update a persona”PATCH /api/personas/:keyUpdates title and description. Omitted fields keep their values; null clears.
Path parameters
| Parameter | Description |
|---|---|
key |
Persona key |
Request body
| Field | Type | Description |
|---|---|---|
title |
string | null |
Display title |
description |
string | null |
The day-in-the-life, one paragraph |
Example request
curl -X PATCH https://lt.acme.dev/api/personas/production-manager \ -H "Authorization: Bearer $LT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "Line Manager" }'Response 200
The updated persona record.
Delete a persona
Section titled “Delete a persona”DELETE /api/personas/:keyRemoves the memberships the persona sustains (or re-homes them to a sibling persona the user still holds), then deletes the persona row. Direct grants stay as they are.
Path parameters
| Parameter | Description |
|---|---|
key |
Persona key |
Example request
curl -X DELETE https://lt.acme.dev/api/personas/production-manager \ -H "Authorization: Bearer $LT_TOKEN"Response 200
{ "deleted": true, "recompute": { "granted": 0, "refreshed": 0, "raised": 0, "removed": 3 } }Link a role
Section titled “Link a role”PUT /api/personas/:key/roles/:roleLinks a role to the persona at a relationship, or updates an existing link’s relationship. The role is created if absent, and every current holder’s memberships are reconciled in the same transaction.
Path parameters
| Parameter | Description |
|---|---|
key |
Persona key |
role |
Role name |
Request body
| Field | Type | Description |
|---|---|---|
relationship |
string |
write-all, write-self, read-all (or write-none) |
Example request
curl -X PUT https://lt.acme.dev/api/personas/production-manager/roles/intake \ -H "Authorization: Bearer $LT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "relationship": "write-all" }'Response 201
{ "role": "intake", "relationship": "write-all", "recompute": { ... } }Unlink a role
Section titled “Unlink a role”DELETE /api/personas/:key/roles/:roleRemoves the link and reconciles every holder.
Path parameters
| Parameter | Description |
|---|---|
key |
Persona key |
role |
Role name |
Example request
curl -X DELETE https://lt.acme.dev/api/personas/production-manager/roles/packing \ -H "Authorization: Bearer $LT_TOKEN"Response 200
{ "unlinked": true, "recompute": { ... } }Seed personas (declarative)
Section titled “Seed personas (declarative)”POST /api/personas/seedDeclares personas and their role links in one idempotent call. The spec is authoritative: titles and descriptions are overlaid, role links are synced (links absent from the spec are pruned), linked roles are ensured as foreign-key targets, and every current holder is reconciled. The SDK twin is lt.personas.seed(specs); declare personas in the same seed pass that declares roles and default_pins.
Request body
| Field | Type | Description |
|---|---|---|
personas |
array |
[{ key, title?, description?, roles: [{ role, relationship }] }] |
Example request
curl -X POST https://lt.acme.dev/api/personas/seed \ -H "Authorization: Bearer $LT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "personas": [ { "key": "production-manager", "title": "Production Manager", "roles": [ { "role": "intake", "relationship": "write-all" }, { "role": "packing", "relationship": "read-all" } ] } ] }'Response 200
{ "personas": 2, "links": 6, "recompute": { ... } }Personas for a user
Section titled “Personas for a user”GET /api/users/:id/personasReturns the personas a user holds and the composed role and scope map.
Path parameters
| Parameter | Description |
|---|---|
id |
User UUID |
Example request
curl https://lt.acme.dev/api/users/b2c3d4e5-.../personas \ -H "Authorization: Bearer $LT_TOKEN"Response 200
{ "personas": [ { "id": "a1b2c3d4-...", "key": "production-manager", "title": "Production Manager", "description": "...", "roles": [ ... ], "assigned_at": "2026-07-10T12:00:00.000Z" } ], "roles": [ { "role": "intake", "read_scope": "all", "write_scope": "all", "granted_by_persona": "production-manager" }, { "role": "review", "read_scope": "all", "write_scope": "all", "granted_by_persona": null } ]}roles lists every membership with the persona sustaining it (null means a direct grant).
Assign a persona
Section titled “Assign a persona”POST /api/users/:id/personasAssigns a persona to a user. Idempotent: re-assigning overlays fresh from the persona’s current role links. The highest allowance wins across held personas, and a direct grant’s scope is only ever raised toward the union.
Path parameters
| Parameter | Description |
|---|---|
id |
User UUID |
Request body
| Field | Type | Description |
|---|---|---|
persona |
string |
Persona key |
Example request
curl -X POST https://lt.acme.dev/api/users/b2c3d4e5-.../personas \ -H "Authorization: Bearer $LT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "persona": "production-manager" }'Response 200
{ "assigned": true, "recompute": { "granted": 3, "refreshed": 0, "raised": 1, "removed": 0 } }Unassign a persona
Section titled “Unassign a persona”DELETE /api/users/:id/personas/:keyRemoves only the memberships the persona sustains; rows another held persona still grants are re-homed to it. Direct grants stay as they are, including a membership added directly on top of a persona grant.
Path parameters
| Parameter | Description |
|---|---|
id |
User UUID |
key |
Persona key |
Example request
curl -X DELETE https://lt.acme.dev/api/users/b2c3d4e5-.../personas/production-manager \ -H "Authorization: Bearer $LT_TOKEN"Response 200
{ "unassigned": true, "recompute": { ... } }Response 404
The user does not hold the persona.
