Skip to main content

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.userId as 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

PathRole
domain/entities/user.entity.tsAggregate behavior and domain events for profile/interests/soft-delete
domain/events/user-events.tsTyped users domain events (UsersDomainEvent)
application/dtos/user.tsUse-case command/query/result types
application/ports/user-repo.port.tsPersistence contract required by use-cases
application/ports/domain-event-publisher.port.tsEvent publishing contract (publish(event))
application/use-cases/*Application orchestration (register, profile, interests, soft-delete)
adapters/persistence/user-repo.impl.tsPrisma implementation of UserRepoPort
adapters/http/routes.tsFastify route registration and schema binding
adapters/http/handlers.tsHTTP -> DTO mapping, auth checks, status mapping
adapters/events/subscribers/*Event-driven infra side effects (SQS welcome email)
composition.tsWires repo, use-cases, event bus, subscribers, and routes

Composition dependencies

createUsersModule({ db, sqsClient, welcomeEmailQueueUrl, authGuard }) requires:

  • db: Prisma client for persistence
  • sqsClient: AWS SQS client for async side effects
  • welcomeEmailQueueUrl: destination queue for welcome email jobs
  • authGuard: shared auth preHandler from auth module

Returned API:

  • routes: Fastify plugin with /users/v1/* endpoints
  • registerByIdentityProviderId: internal use-case entrypoint used by auth/webhook flows
  • softDeleteByUserId: 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(...).

MethodPathBehavior
GET/users/v1/profileReturns current user profile (200) or 404 if profile is missing.
POST/users/v1/profileUpserts profile for current user; returns updated profile (200) or 404 if user is missing/deleted.
GET/users/v1/interestsReturns current user interests (200) or 404 if user is missing/deleted.
POST/users/v1/interestsReplaces current user interests (200) or 404 if user is missing/deleted.
GET/users/v1/meReturns auth identity pair { userId, identityProviderUserId }.

Route schemas are imported from shared contracts:

  • contracts/user
  • contracts/system

Application use-cases

registerByIdentityProviderId

  • input: RegisterUserCommand { identityProviderUserId }
  • delegates to repo registration logic
  • publishes users.user-registered with isNewUser metadata

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-registered additionally triggers:
    • createPublishWelcomeEmailOnUserRegisteredSubscriber(...)
    • publishes SQS message kind: "welcome-email-request" only for isNewUser === true

Event bus dispatch uses createEventBusPublisher from shared kernel and runs handlers in parallel.

Runtime flow summary

  1. HTTP request enters users route with authGuard.
  2. Handler builds application DTO from auth context + request body/query.
  3. Use-case executes domain and persistence operations.
  4. Use-case publishes domain events via injected publisher.
  5. Event bus routes to subscribers (log, SQS welcome email).
  6. Handler returns schema-aligned response.