Users Module
Overview
The users module manages system user accounts that are tightly coupled to Firebase Authentication. It also owns the multi-step business onboarding flow through which new users create their first business, location, and billing configuration.
Module path
apps/backend/src/users/
Architecture
The module follows the Hexagonal Architecture pattern used project-wide:
users/
├── domain/ # Ports (interfaces)
│ ├── users-repository.domain.ts # IUsersRepository
│ └── users-service.domain.ts # IUsersService, ICreateUserParameters
├── application/ # Use cases
│ ├── users.service.ts # User CRUD + onboarding status
│ └── onboarding-orchestrator.service.ts # Multi-step onboarding logic
├── infrastructure/ # Adapters
│ └── users.repository.ts # Kysely PostgreSQL implementation
└── interfaces/ # HTTP adapters
├── me.controller.ts # GET /users/me
├── users.controller.ts # CRUD + businesses-and-locations
├── me-onboarding.controller.ts # /users/me/onboarding/* endpoints
├── firebase.controller.ts # GET /users/firebase
├── dtos/ # Request bodies
└── query/ # Pagination/sort query params
Layer Responsibilities
| Layer | Responsibility |
|---|---|
| Domain | Repository and service interfaces (ports). Framework-agnostic. |
| Application | Use cases (UsersService, OnboardingOrchestratorService). Orchestrates domain operations. |
| Infrastructure | Kysely PostgreSQL adapter. Implements IUsersRepository. |
| Interfaces | HTTP controllers, DTOs, query objects. Thin request/response mapping. |
Dependency Flow
Controllers → Application Services → Domain Interfaces ← Infrastructure Adapters
All cross-table queries delegate to their respective module services (BusinessesService, LocationsService, BusinessUsersService). The UsersRepository only queries the user table.
Domain Concepts
User
A user record represents an authenticated system identity. It is linked 1:1 with a Firebase Auth account via firebaseOauthUid. Users are not employees — see docs/employees/README.md for the distinction.
user
├── id UUID (PK)
├── firebaseOauthUid Firebase UID
├── fullName
├── email
├── phone
├── systemUniqueRoleName null | "admin" | "root" (super-admin bypass)
├── createdAt / updatedAt / updatedBy
Firebase ↔ DB User Synchronisation
- Firebase is the auth source of truth; the DB
usertable is the business data source of truth. - On first login via
POST /auth/firebase, a DB user record is created and thedb_user_idcustom claim is written back to Firebase. - Most authenticated endpoints extract the DB user ID from
db_user_idclaim rather than looking it up by Firebase UID on every request.
OnboardingStep
A state machine on the business.onboardingStatus column:
business → location → billing → complete → done
Application Services
UsersService
Core use-case service. Responsibilities:
| Method | Description |
|---|---|
createUser | Inserts DB user, syncs db_user_id Firebase claim |
findUserById | Lookup by DB ID |
findUserByFirebaseUid | Lookup by Firebase UID |
getOrCreateUser | Upsert on first login; updates fullName if changed |
getUser | Alias for findUserByFirebaseUid (used by AuthService) |
getUserBusinessesAndLocations | Returns businesses + roles + locations for current user; auto-creates user record |
getUserOnboardingStatus | Returns current onboarding step + typed snapshots |
getStatusCreatingBusiness | Same but scoped to an in-progress additional business |
updateUser | Updates fullName |
getAllUsers | Paginated list (sortable by fullName, email, createdAt) |
OnboardingOrchestratorService
Coordinates multi-step onboarding. Each method wraps a sequence of cross-module operations:
| Method | Description |
|---|---|
upsertOnboardingBusiness | Creates/updates business, creates Owner business_user, syncs Firebase claims |
upsertUserLocation | Resolves Google Place, creates/updates address + location, advances status to billing |
patchOnboardingBill | Saves legal name, tax ID, FEL credentials |
patchOnboardingStatus | Advances onboardingStatus; sets onboarding_completed Firebase claim when done |
Onboarding Response Types
The onboarding status endpoints return a typed OnboardingStatusResponse:
interface OnboardingStatusResponse {
step: OnboardingStep; // "business" | "location" | "billing" | "complete" | "done"
business: OnboardingBusinessSummary | null;
location: OnboardingLocationSnapshot | null;
billing: OnboardingBillingSummary | null;
}
interface OnboardingLocationSnapshot {
id: string;
name: string | null;
placeId: string | null;
address: { lineOne: string | null; lineTwo: string | null } | null;
}
API Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /users | Bearer | Create a user (links Firebase → DB) |
GET | /users | Bearer | List all users (paginated, sortable) |
GET | /users/me | Bearer | Get current authenticated user |
GET | /users/me/businesses-and-locations | Bearer | Get businesses + locations for current user |
PATCH | /users/:id | Bearer | Update user fullName |
GET | /users/firebase | Bearer | Get user by Firebase UID |
POST | /users/me/onboarding/business | Public | Create/update onboarding business |
POST | /users/me/onboarding/:businessId/location | Bearer | Create/update onboarding location |
PATCH | /users/me/onboarding/:businessId/bill | Bearer | Save billing / FEL info |
GET | /users/me/onboarding/status | Bearer | Get current onboarding step |
GET | /users/me/onboarding/creating-business | Bearer | Get status of an in-progress additional business |
PATCH | /users/me/onboarding/:businessId/status | Bearer | Advance onboarding status |
POST /users/me/onboarding/businessis@IsPublic()because the DB user record may not exist yet on first call.extractDbUserIdFromRequestcreates it if missing.
Bruno API Collection
api-client/flowpos/collections/users/
├── user.yml POST /users
├── users.yml GET /users
├── update-user.yml PATCH /users/:id
├── businesses and locations.yml GET /users/me/businesses-and-locations
└── me/
├── users-me.yml GET /users/me
├── users-firebase.yml GET /users/firebase
└── onboarding/
├── users-me-onboarding.yml GET /users/me/onboarding/status
├── creating-business.yml GET /users/me/onboarding/creating-business
├── business.yml POST /users/me/onboarding/business
├── location.yml POST /users/me/onboarding/:businessId/location
├── bill.yml PATCH /users/me/onboarding/:businessId/bill
└── done.yml PATCH /users/me/onboarding/:businessId/status
Onboarding Flow
See also: docs/onboarding/Onboarding-System-Architecture.md
1. POST /users/me/onboarding/business
→ Creates business (onboardingStatus = "location")
→ Creates business_user (Owner role)
→ Syncs Firebase claim: role_by_business_id
2. POST /users/me/onboarding/:businessId/location
→ Resolves Google Place (optional)
→ Creates address + location
→ Advances onboardingStatus → "billing"
3. PATCH /users/me/onboarding/:businessId/bill
→ Saves legalName, taxId, FEL credentials
4. PATCH /users/me/onboarding/:businessId/status { onboardingStatus: "done" }
→ Sets onboardingStatus = "done"
→ Sets Firebase claim: onboarding_completed = true
Design Decisions
@IsPublic() on the business upsert endpoint
The onboarding business endpoint is public because at the moment the user first hits it, no DB user record exists. extractDbUserIdFromRequest handles Firebase token validation and auto-creates the DB record transparently.
getOrCreateUser pattern
GET /users/me/businesses-and-locations uses get-or-create instead of hard-failing when the user does not exist. This prevents race conditions on first login when the frontend calls this endpoint immediately after Firebase sign-in before POST /auth/firebase completes.
Cross-module delegation for onboarding status
The UsersService delegates all non-user queries to their respective services (BusinessesService.getBusinessById, BusinessesService.findBusinessByUserId, LocationsService.findManyLocations). This maintains SRP — the UsersRepository only queries the user table.
FEL credentials storage
FEL (Guatemalan e-invoicing) credentials are stored encrypted in business.felCertifierConfig (JSONB). The schema for this field is { felUsername, felPassword, felAccessCode, felToken }.
Typed onboarding snapshots
The OnboardingLocationSnapshot interface provides a typed, minimal projection of location data for onboarding status responses, avoiding unknown types in the API contract.
Known Follow-ups
- Add transactional guarantees to
upsertOnboardingBusiness(business + business_user creation should be atomic). - Replace concrete
UsersRepositoryinjection inUsersServicewith anIUsersRepositoryDI token (pending project-wide convention adoption).