User Service & User Document Model
tektree beginner 4 min read
ELI5
A user document is a name tag with two IDs on the back: the badge number Mongo printed (_id) and the staff ID HR uses (uid). Profile and preferences are little stickers on the back — bio, avatar, theme — and one important sticker says which plan they paid for.
Technical Deep Dive
User identity lives in two places that need to agree: the new services/user-service (microservice form) and the legacy services/api monolith (which still owns the canonical Mongo schema in production today).
Document Class Diagram
classDiagram class UserDocument_legacy { +ObjectID ID +string UID +string Email +bool EmailVerified +string Image +string FirstName +string LastName +string Name +string Handle } class User_microservice { +string ID +string Email +string DisplayName +string AvatarURL +string Tier +UserProfile Profile +UserPreferences Preferences +time.Time CreatedAt +time.Time UpdatedAt } class UserProfile { +string Bio +string Location +string Website +[]string ExpertiseAreas } class UserPreferences { +bool EmailNotifications +bool PushNotifications +string Theme +string Timezone +string Language } User_microservice "1" --> "1" UserProfile User_microservice "1" --> "1" UserPreferencesStorage
- Collection:
users(MongoDB) - Indexes (per
docs/docs/architecture/DATA_MODELS.md):emailunique,handleunique,uidunique,created_at - Default tier:
TierFree(services/user-service/internal/models/models.go:29-45) - Stable external id:
uid(legacy) — used in JWTsuband inX-User-IDheader values; the Mongo_idis internal-only
Routes
services/user-service/cmd/server/main.go:53-72 registers:
| Method | Path | Handler |
|---|---|---|
| GET | /health | HealthCheck |
| GET | /api/v1/users/:id | GetUser |
| POST | /api/v1/users | CreateUser |
| PUT | /api/v1/users/:id | UpdateUser |
| DELETE | /api/v1/users/:id | DeleteUser |
The handlers do not validate the X-User-ID header today — they trust the gateway. Authorization (e.g. “you may only PUT your own user”) is the caller’s responsibility for now and is the most important hardening item before the legacy monolith is fully retired.
Schema Drift Risk
services/api/models/user.models.go (UserDocument) uses *string for almost every field (omitempty bson) while the new User struct uses non-pointer values. A migration that copies legacy → microservice must decide on null-vs-empty semantics field-by-field; do not assume they round-trip.
Key Terms
uid→ stable external id, surfaces in JWTsubandX-User-ID._id→ MongoDB ObjectID; internal-only.tier→ string column, defaultfree; the gateway reads it via the JWT, not via the DB.handle→ unique vanity name; separate from email and uid.
Q&A
Q: A new login uses a fresh email but the same handle as an existing soft-deleted account. What happens?
A: The unique index on handle rejects the insert. Soft deletes do not free the index slot; either hard-delete or rename on archive.
Q: Where does the tier field flip when a user upgrades?
A: Payment-service writes the new tier into the user record (or a dedicated subscriptions record) on a subscription.created / subscription.updated Polar webhook. The next access-token mint reflects it.
Q: Why are legacy fields nullable pointers when the new ones are not?
A: The legacy collection accumulated optional profile data over years and uses omitempty to keep documents lean. The new schema codifies sensible empty-string defaults so consumers can avoid nil checks.
Examples
Adding pronouns to user profile: append to UserProfile in services/user-service/internal/models/models.go, mirror in libs/proto/user.proto’s UserProfile message, regenerate proto, expose in the legacy UserDocument as *string Pronouns to keep the Mongo schema compatible.
neighbors on the map
- Multi-Strategy Authentication debugging 401 errors across different clients
- LORE RBAC & Airlock Auth Flow implementing authentication in a new LORE page