LaWalletdocs
Architecture

Roles & Permissions

LaWallet NWC RBAC model: four hierarchical roles, granular permissions, and how the caller's role is resolved.

LaWallet NWC uses an RBAC model with four hierarchical roles plus a PUBLIC level for unauthenticated calls. Each role has a fixed set of granular permissions; route guards check either a minimum role or a specific permission, depending on the case.

Hierarchy

PUBLIC  →  USER  <  VIEWER  <  OPERATOR  <  ADMIN

A higher role includes everything a lower role can do. Guards use hasRole(actual, required), which compares positions in the hierarchy.

RoleWho it isPrivilege summary
PUBLICUnauthenticated callerOpen endpoints: LUD-16 payRequest/callback, /.well-known/verify, /api/setup/status, and POST /api/jwt token exchange.
USERAny authenticated pubkey with no additional permissionsReads and edits its own data (LUD-16 and its own NWC). Cannot access the admin panel.
VIEWERAuditor or observerRead-only access to cards, designs, addresses, ntags, users, settings, and the activity log.
OPERATORDay-to-day operatorVIEWER access plus write access to cards, designs, addresses, and ntags. Cannot change settings or other users' roles.
ADMINSystem rootEvery permission, without exception. Automatically assigned to the first pubkey that claims the bootstrap endpoint (POST /api/admin/assign).

Permission matrix

Permissions are defined as an enum in lib/auth/permissions.ts, and each role maps to a fixed permission list.

PermissionUSERVIEWEROPERATORADMIN
settings:read
settings:write
users:read
users:write
users:manage_roles
cards:read
cards:write
card_designs:read
card_designs:write
addresses:read
addresses:write
ntags:read
ntags:write
activity:read

USER has none of these platform permissions. It is the default role for authenticated pubkeys operating only on their own identity.

How the caller's role is resolved

Every authenticated request (NIP-98 or Bearer JWT) passes through resolveRole(pubkey) in lib/auth/resolve-role.ts:

  1. It looks up a User with that pubkey in the database. If the user has a role other than USER, it returns that role.
  2. Otherwise, it checks the root setting (for compatibility with bootstrap before the User table). If the pubkey matches, the role is ADMIN.
  3. In every other case, the role is USER.

The JWT embeds the role when it is issued, but that claim is only a hint: session tokens are stateless and cannot be revoked, so resolveRole(pubkey) re-runs on every request and the freshly resolved role is what authorizes it. Demoting or deleting a user takes effect immediately, without waiting for the token to expire.

Two deliberate exceptions:

  • Device tokens (kind: 'device') keep the role they were minted with. Their scopes claim is the authoritative restriction — an explicit subset of the minting admin's permissions — so re-resolving the owner's role would widen, not narrow, what the token can do.
  • Session lifetimes are capped at 24 hours (MAX_SESSION_JWT_SECONDS), since a leaked stateless token stays valid until it expires.

How a role is assigned

  • Bootstrap (first admin): the first pubkey to call POST /api/admin/assign with NIP-98 signs the root setting and becomes ADMIN. See the API Playground and the JWT Authentication guide.
  • Promoting or demoting users: requires the users:manage_roles permission (ADMIN only). Endpoint: PUT /api/users/{userId}/role.
  • Self-demotion is blocked: an admin cannot lower its own role, and the system prevents the platform from having no ADMIN users.

How a route checks a role

Protected routes use helpers in lib/auth/:

  • requireRole(req, Role.OPERATOR) — requires the minimum OPERATOR role. Returns 403 with AuthorizationError if the caller does not qualify.
  • requirePermission(req, Permission.CARDS_WRITE) — performs a granular check against the permission map.
  • withAdminAuth(handler) — HOF in lib/admin-auth.ts that validates NIP-98 and requires ADMIN. Bootstrap endpoints use it.

Routes that accept both methods (NIP-98 or JWT) use lib/auth/unified-auth.ts, which detects the Authorization header and applies the appropriate flow before resolving the role.

In the API Playground

In the interactive API Playground, every endpoint shows a badge with the required minimum role (PUBLIC, USER, VIEWER, OPERATOR, ADMIN). The colors help you quickly identify the endpoints available to each role before testing them with your NIP-07 signer.

On this page