CRUMB a card from devarno-cloud

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" UserPreferences

Storage

  • Collection: users (MongoDB)
  • Indexes (per docs/docs/architecture/DATA_MODELS.md): email unique, handle unique, uid unique, created_at
  • Default tier: TierFree (services/user-service/internal/models/models.go:29-45)
  • Stable external id: uid (legacy) — used in JWT sub and in X-User-ID header values; the Mongo _id is internal-only

Routes

services/user-service/cmd/server/main.go:53-72 registers:

MethodPathHandler
GET/healthHealthCheck
GET/api/v1/users/:idGetUser
POST/api/v1/usersCreateUser
PUT/api/v1/users/:idUpdateUser
DELETE/api/v1/users/:idDeleteUser

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 JWT sub and X-User-ID.
  • _id → MongoDB ObjectID; internal-only.
  • tier → string column, default free; 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