--- type: api title: Setup Endpoint description: Dedicated POST /api/v1/setup/create-admin endpoint used only while no administrator exists. tags: [api, setup, bootstrap, first-admin] timestamp: 2026-07-21T14:43:00Z --- # Endpoint | Method | Path | Auth | CSRF | Rate limited | |--------|-------------------------------|------|--------|--------------| | `POST` | `/api/v1/setup/create-admin` | `@Public()` | Skipped (path is in `CsrfMiddleware.SKIP_PATH_PREFIXES`) | Yes (per-IP via `RegistrationRateLimitService`) | # Request body Validated by `backend/src/modules/setup/dto/create-admin.dto.ts` (zod): | Field | Type | Constraints | |-------------------|--------|------------------------------------------------------------------------------| | `username` | string | 3–32 chars, regex `^[a-zA-Z0-9_.-]+$` | | `password` | string | 1–256 chars; additionally passes `validatePassword` (Argon2 policy). | | `passwordConfirm` | string | Must equal `password` (zod refine → `VALIDATION_FAILED`, `passwordConfirm`).| # Successful response (201) ```json { "accessToken": "", "expiresIn": 900, "user": { "id": "", "username": "first.admin", "role": "admin" } } ``` The refresh token is set as an `HttpOnly` cookie via `setRefreshCookie(res, session.refreshToken, config)` so the SPA can keep refreshing without storing the raw token in JS. # Error envelope | HTTP | `code` | When | |------|---------------------|----------------------------------------------------------------------| | 400 | `VALIDATION_FAILED` | zod body validation failed (length, regex, or `passwordConfirm`). | | 400 | `WEAK_PASSWORD` | Argon2 policy rejects the password (length / mixed-case requirement). | | 404 | `NOT_FOUND` | An admin already exists — the route is hidden once initialized. | | 409 | `USERNAME_TAKEN` | Username collision (reachable only during a race before init). | | 429 | `RATE_LIMITED` | `RegistrationRateLimitService` blocked the IP. | # Wiring * Registered in `backend/src/app.module.ts` via `SetupModule`. * `SetupController` (`backend/src/modules/setup/setup.controller.ts`) is `@Public()` and uses `ZodValidationPipe(CreateAdminDtoSchema)`. * `SetupService.createAdmin` runs everything inside a single `DataSource.transaction`: admin count check → username uniqueness check → Argon2id hash → insert `UserEntity(role='admin', status='enabled')` → mint session via the static `AuthService.createSession`. * CSRF is bypassed for `/api/v1/setup/create-admin` so the first caller (before any cookie exists) is not blocked. The middleware still mints a `csrf` cookie on the response so subsequent calls are covered. # Frontend consumer `frontend/src/app/features/setup/setup-create-admin.service.ts` calls this endpoint and normalizes the response into the discriminated union `CreateAdminResult`: ```ts type CreateAdminResult = CreateAdminSuccess | CreateAdminFailure; ``` `CreateAdminFailure` exposes `{ ok: false, status, code, message }` so the component can switch on `code === 'USERNAME_TAKEN'` to surface a non-retryable inline error and offer a Retry button for everything else. # See also - [First-Run Bootstrap Guide](/guides/bootstrap.md) - [Backend Module Map](/architecture/backend-modules.md) - [Bootstrap Service](/architecture/frontend-structure.md)