Users Module
The Users module owns the canonical user record and the user-profile sub-resource. It is not responsible for authentication itself — that lives in the standalone auth server and reaches this module read-only over gRPC. What this module does own: account CRUD, search, role-gated admin lookups, and the editable profile fields the dashboard’s “User Profile” page renders.
Class Diagram
Fundamental Purpose
A user record is the platform-wide identity. It carries:
- Identity —
id(UUID),email,username(lowercase, unique). - System role — one of
admin,moderator,user. Role gates are enforced insideService(see Role gates below), not at the route layer. - Profile fields —
display_name,avatar_url,bio,location,website, plus a free-formmetadataJSON column. These are owned by the profile sub-resource (/users/profile) so that profile edits never require touching the auth-critical fields (email, username, password hash). - Password hash — bcrypt-hashed; never read by this module’s repository. Auth-related lookups go through the auth server’s repository.
- Lifecycle — soft-deleted via
deleted_at. List/search filter out deleted users by default.
HTTP Surface
Routes are mounted under <APIPrefix>/users (default /api/v1/users). All routes require a valid JWT or API key.
| Method | Path | Handler | Notes |
|---|---|---|---|
GET | / | List | admin/moderator — paginated user list |
POST | / | Create | admin — create a user (e.g. seeded operator account) |
GET | /search?q=... | Search | admin/moderator — substring match on email + username |
GET | /by-email?email=... | GetByEmail | Caller must be the target OR have admin/moderator role |
GET | /by-username?u=... | GetByUsername | Same authorization rules as by-email |
GET | /profile | GetProfile | Returns the calling user’s profile |
PUT | /profile | UpdateProfile | Updates only profile fields (subset of User) |
GET | /{id} | GetByID | Same authorization rules as by-email |
PUT | /{id} | Update | Caller must be the target user OR admin |
DELETE | /{id} | Delete | Caller must be the target user OR admin |
Special Behaviors
Role gates are enforced in the Service, not the router
List, Search, and Create go through Service.checkListOrSearchPermission before the repository is ever touched (apps/api/modules/users/service.go:253). The router doesn’t know about roles — it just authenticates the JWT. This keeps role logic together with the rest of the business rules and makes it directly testable without an HTTP harness.
Profile vs. base record split
The /users/{id} surface is for administrative changes (email, username, role). The /users/profile surface is for the calling user’s cosmetic fields (display_name, avatar_url, etc.). Two reasons:
- The dashboard’s “Edit profile” page needs neither admin role nor the ability to change the email — separating the surfaces removes an entire class of “did I mean to expose this?” bugs.
- Cache invalidation is finer-grained: a profile update doesn’t have to bust caches keyed by email/username.
UpdateProfile’s payload type (UpdateProfileRequest) is a strict subset of UpdateUserInput — the SDK enforces this at compile time.
Soft-delete semantics
DELETE /users/{id} sets deleted_at. The auth server’s ValidateToken RPC also checks deleted_at — a JWT issued before a soft-delete is rejected on the next request. This is one of the few cross-process invariants the platform enforces; both servers must read the same column for it to work.
Author denormalization
Other modules (most prominently Blobs) embed a trimmed BlobAuthor summary ({id, username, display_name, avatar_url}) on responses. This is computed in the consumer module, not here — the Users module only exposes full records and lets callers project them. That keeps user-record changes from forcing schema migrations across every embedding consumer.
SDK Surface
The TypeScript SDK exposes this module as the UsersService on blobs.users:
UsersService— full method reference (getById,getByEmail,getByUsername,list,search,update,delete,getProfile,updateProfile).User— canonical wire shape.UpdateUserInput— for admin updates.UpdateProfileRequest— strict subset for profile edits.actions/user—UpdateUserActionfor undo/redo on the profile editor. Auth flows (login/register/refresh/logout) are deliberately not action-ized — their inverses are unsafe or undefined; see the user actions notes for details.
Cross-Module Integration
| Module | Relationship |
|---|---|
| Auth server | Owns the auth-critical fields (email, password_hash, refresh_tokens). Reads this module’s row via shared Postgres. The two services agree on the schema; only one writes any given column. |
| Blobs | Embeds BlobAuthor on responses. Updates here propagate naturally on next read — no event subscription needed. |
| Permissions | Reads role to evaluate system-level role gates. |
| Settings | Per-user settings keyed by user_id. |
API Reference
Open the interactive API reference and jump to the /users tag for live request/response examples.