First login

Where the UI is served

The Installer serves its own single-page app and its API from the same container and the same origin:

text
http://<host>:<NEST_PORT>/

NEST_PORT defaults to 8000. Every route except /api/* is served by the app, so opening the bare host address is enough — there is no separate port for the UI and no /ui path.

How the first administrator account is created

Important

There is no default account, no seeded credential, no generated password, no environment variable and no CLI command that creates the Installer's first user. The first administrator is created by a human, in the browser, on the first visit.

That visit is self-registering, and only while the Installer has no users at all:

  1. The app calls GET /api/user/exists (a public endpoint) before it draws anything.
  2. When it reports no users, the app shows the registration form — Create admin account — instead of the login form. It asks for an email address, a password and a password confirmation, and it then authenticates you with those same credentials.
  3. Submitting calls POST /api/user/register/initial, a public endpoint, with {"email": "...", "password": "..."}. The new account is created with the role admin and the response returns the created user.

The endpoint refuses to work a second time. Once any user exists it answers HTTP 403 with "Initial registration is not allowed once a user exists.", and the app switches to the login form permanently, with the hint "Admin user already exists. Contact your team to set up your account."

If somebody else reaches the UI before you do, they become the administrative account. Complete the first login before the host is reachable from anywhere but your own management path.

Choose the password deliberately — the initial registration enforces a policy:

RuleValue
Minimum length8 characters
Uppercaseat least 1
Lowercaseat least 1
Digitsat least 1
Symbolsat least 1

Failing validation returns: "Password must be at least 8 characters and contain uppercase, lowercase, number, and special character."

Verify the account exists (no authentication needed):

bash
curl -sS http://localhost:8000/api/user/exists

Once the account is created this reports true, and the registration form will never appear again.

Signing in afterwards

Use the login form, or call the API directly:

bash
curl -sS -X POST http://localhost:8000/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"..."}'

The email is matched case-insensitively — it is lower-cased before lookup. The responses you may see:

SituationResponse
Wrong email or passwordHTTP 401, Invalid email or password — the same message for both, so an attacker cannot tell whether an address exists
Correct credentials, but the account was disabledHTTP 403, Account is deactivated
SuccessHTTP 200 with data.user (id, email, role), data.accessToken and data.expiresIn

What you should change immediately

Do thisWhy
Change the administrator password (account menu → change password) if it was typed by anybody else, transmitted over a channel you do not control, or reused from another systemThe first account is the whole control plane. POST /api/user/:id/change-password needs the current password, rejects reusing the same one (You can't set the same password), and revokes every refresh token the user holds
Re-enter the Azure client secret if a third party supplied itIt is stored encrypted in the Installer's database and is what allows image pulls and artifact downloads. Changing it is PUT /api/system-secrets/azure
Create one named account per administrator and use no shared loginsThe deployment audit trail resolves the acting user; a shared account destroys the attribution
Put an authentication gate in front of the UI extra to the product if the host is reachable beyond your management networkThe Installer ships no second factor — no TOTP, no WebAuthn, no OIDC
Leave SWAGGER_ENABLED=falseSwagger publishes the entire API surface at /api/docs

Changing a password and resetting one are different operations, and the Installer has no self-service password recovery: there is no email flow and no "forgot password" link. If the only administrator loses their password, another admin must reset it with POST /api/user/:id/reset-password. Keep more than one administrator for this reason alone. See Who should have access.

Session and token behaviour

Each sign-in issues two JSON Web Tokens, signed with the JWT_SECRET that the backend generated into its secrets volume on first boot. Their lifetimes come from .env:

SettingDefaultEffect
JWT_ACCESS_EXPIRY30mLife of the access token, in the format the ms library accepts (30m, 1h, 900s)
JWT_REFRESH_EXPIRY7dLife of the refresh token

The access token carries type: "access" and the refresh token type: "refresh"; the backend rejects a refresh token presented as an access token. If the variables are unset, the code falls back to 1h for the access token and 7d for the refresh token.

Where the tokens live. The login response both sets cookies and returns the access token in the body. The cookies are the durable mechanism:

CookiePathNotes
delamain_access_token/apihttpOnly, SameSite=Strict by default
delamain_refresh_token/api/authhttpOnly, single-use

Both are marked Secure when the request itself is HTTPS, or when a proxy sets x-forwarded-proto: https. Both are SameSite=Lax only for a cross-origin call from localhost, and SameSite=None plus Partitioned only when a cross-origin call arrives over HTTPS. When the browser host matches the request host, no cookie domain is set; in production, when they differ, the cookie is scoped to the last two labels of the host.

Warning

Besides the cookies, the browser app stores the access token in localStorage under manager:access-token, and the user object under manager:auth-user. Any script running in the page origin can read those values. Keep the UI on a trusted management network and do not add third party scripts to it.

Rotation and invalidation. The refresh token is single-use: POST /api/auth/refresh revokes the presented token and issues a new pair. A fresh sign-in invalidates all of that user's outstanding refresh tokens, as does a password change or an administrative password reset. POST /api/auth/logout revokes the presented refresh token and clears both cookies; it returns HTTP 204.

Expired refresh tokens are removed by a scheduled cleanup task, so the table does not grow without bound.

Consequences worth planning for:

Next: Post-install checklist.