Users Module
This document describes the current users module architecture in apps/api/src/modules/users, including HTTP endpoints, use-cases, persistence, and domain-event wiring.
Purpose
The users module:
- owns internal user lifecycle data (
userId, profile, interests, soft-delete state) - exposes authenticated HTTP endpoints under
/users/v1/* - treats
request.auth.userIdas the internal DB user id resolved by auth module - emits domain events for important user actions
- triggers side effects (logging, welcome email queue publish) through event subscribers
Module structure
| Path | Role |
|---|---|
domain/entities/user.entity.ts | Aggregate behavior and domain events for profile/interests/soft-delete |
domain/events/user-events.ts | Typed users domain events (UsersDomainEvent) |
application/dtos/user.ts | Use-case command/query/result types |
application/ports/user-repo.port.ts | Persistence contract required by use-cases |
application/ports/domain-event-publisher.port.ts | Event publishing contract (publish(event)) |
application/use-cases/* | Application orchestration (register, profile, interests, soft-delete) |
adapters/persistence/user-repo.impl.ts | Prisma implementation of UserRepoPort |
adapters/http/routes.ts | Fastify route registration and schema binding |
adapters/http/handlers.ts | HTTP -> DTO mapping, auth checks, status mapping |
adapters/events/subscribers/* | Event-driven infra side effects (SQS welcome email) |
composition.ts | Wires repo, use-cases, event bus, subscribers, and routes |
Composition dependencies
createUsersModule({ db, sqsClient, welcomeEmailQueueUrl, authGuard }) requires:
db: Prisma client for persistencesqsClient: AWS SQS client for async side effectswelcomeEmailQueueUrl: destination queue for welcome email jobsauthGuard: shared auth preHandler from auth module
Returned API:
routes: Fastify plugin with/users/v1/*endpointsregisterByIdentityProviderId: internal use-case entrypoint used by auth/webhook flowssoftDeleteByUserId: internal use-case entrypoint (not exposed via current HTTP users routes)
Auth model in users handlers
Users routes use preHandler: authGuard. Handlers read:
type UsersRequestAuth = {
isAuthenticated: boolean;
userId?: string; // internal user id
identityProviderUserId?: string; // Clerk id
};
If auth context is missing, handlers return 401 { message: "Unauthorized" }.
HTTP routes (/users)
All current users routes are authenticated and mounted from usersRoutes(...).
| Method | Path | Behavior |
|---|---|---|
GET | /users/v1/profile | Returns current user profile (200) or 404 if profile is missing. |
POST | /users/v1/profile | Upserts profile for current user; returns updated profile (200) or 404 if user is missing/deleted. |
GET | /users/v1/interests | Returns current user interests (200) or 404 if user is missing/deleted. |
POST | /users/v1/interests | Replaces current user interests (200) or 404 if user is missing/deleted. |
GET | /users/v1/me | Returns auth identity pair { userId, identityProviderUserId }. |
Route schemas are imported from shared contracts:
contracts/usercontracts/system
Application use-cases
registerByIdentityProviderId
- input:
RegisterUserCommand { identityProviderUserId } - delegates to repo registration logic
- publishes
users.user-registeredwithisNewUsermetadata
updateProfile
- requires existing non-deleted user aggregate
- updates aggregate profile via domain entity
- persists with repo
updateProfile(...) - publishes domain events pulled from entity
setUserInterests
- requires existing non-deleted user aggregate
- updates aggregate interests via domain entity
- persists using repo replace strategy
- publishes domain events pulled from entity
getUserInterests
- reads interests from repo
- when found, also publishes
users.user-interests-read
getProfile
- read-only passthrough to repo
getProfile(...) - does not publish events
softDeleteByUserId
- internal use-case (returned by module, not part of users HTTP routes)
- soft-deletes user when currently active
- publishes entity events only when delete succeeds
Persistence behavior (Prisma adapter)
createUserRepo implements UserRepoPort with these notable rules:
- registration is idempotent by
identityProviderUserId - previously soft-deleted users are restored (
deletedAt -> null) on registration - concurrent register conflicts (
P2002) are handled by read-after-conflict fallback - profile writes use
profile.upsert(...) - interests writes replace all links (
deleteMany+ upsert tags + create links) - all read methods ignore soft-deleted users (
deletedAt: null)
Domain events and subscribers
Users module wires a typed event bus in composition.ts:
- all current event types log through
createLogDomainEventHandler(...) users.user-registeredadditionally triggers:createPublishWelcomeEmailOnUserRegisteredSubscriber(...)- publishes SQS message
kind: "welcome-email-request"only forisNewUser === true
Event bus dispatch uses createEventBusPublisher from shared kernel and runs handlers in parallel.
Runtime flow summary
- HTTP request enters users route with
authGuard. - Handler builds application DTO from auth context + request body/query.
- Use-case executes domain and persistence operations.
- Use-case publishes domain events via injected publisher.
- Event bus routes to subscribers (log, SQS welcome email).
- Handler returns schema-aligned response.
Related docs
- Auth Module - how
request.authis created. - Domain Events - event bus and subscriber architecture.
- Contracts and Types - DTO vs shared contract boundaries.