Getting started
Prerequisites
- .NET 10 SDK
- PostgreSQL — the API’s relational database.
- Python 3 — only to run the migration menu script.
- The pinned EF Core CLI tool. Restore it once after cloning:
dotnet tool restore
Building from source, running the test suites and the other contributor tooling are covered in Contributing .
Clone
The documentation site’s theme is a git submodule, so clone with submodules (or initialise them afterwards):
git clone --recurse-submodules https://github.com/artur-rios/heimdall-api.git
Configure
Environments/.env is a tracked template listing every variable the API reads. Real values live
in per-environment files that are gitignored. Create your local one before the first run:
cp src/Presentation/ArturRios.Heimdall.WebApi/Environments/.env src/Presentation/ArturRios.Heimdall.WebApi/Environments/.env.local
How the environment picks its files
ASPNETCORE_ENVIRONMENT selects two files, both resolved next to the built assembly:
| Environment | Variables | App settings |
|---|---|---|
Local (what the launch profiles set) | Environments/.env.local | Settings/appsettings.Local.json |
Development | Environments/.env.development | Settings/appsettings.Development.json |
Staging | Environments/.env.staging | Settings/appsettings.Staging.json |
Production | Environments/.env.production | Settings/appsettings.Production.json |
Both sets are gitignored — the env files hold credentials, and the app settings are per-machine.
They are for running from source: the image carries no Environments/ file, and a container takes
its variables from the env file Compose is given instead — see Environments and deployment
,
where local and development run as Development, homologation as Staging and production as
Production.
Settings/appsettings.json is tracked and loaded first, so a missing environment file costs defaults
rather than the whole configuration.
When the file for the current environment does not exist, the loader falls back to the default
environment, Local, and says so:
Could not find variables for Development environment. Loading default environment Local instead...
That line is easy to miss in a start-up log, and the run then succeeds against the wrong
configuration — which is why the launch profiles name Local outright rather than leaving
Development to fall back into it.
Required — the API will not start without these
| Variable | Value |
|---|---|
HEIMDALL_DATA_CONNECTIONSTRING | A PostgreSQL connection string |
HEIMDALL_DATA_DATABASETYPE | PostgreSql |
HEIMDALL_AUTH_TOKEN_SECRET | The JWT signing secret: at least 32 bytes, ASCII only. Startup throws if it is unset, shorter or holds another character, naming the variable and never the value |
HEIMDALL_APPLICATION_TOKEN_AUDIENCE | The aud of application tokens (UC-56). Startup throws if it is unset, or equal to HEIMDALL_AUTH_TOKEN_AUDIENCE or to the .2fa-challenge or .subject-rights audience derived from it: a distinct audience is what stops a consumer taking an application token for a person’s. docker-compose.yml defaults it to heimdall-applications and the docker/*.env.example templates preset that value; the Web API’s Environments/.env holds only a placeholder |
HEIMDALL_MASTER_USER_NAME / _EMAIL / _PASSWORD | Credentials for the first system administrator. Consulted only when the database holds no system admin yet, but the seeder refuses to start without them in that case |
Optional — each falls back to a default
| Variable | Default |
|---|---|
HEIMDALL_AUTH_TOKEN_ISSUER / _AUDIENCE | Empty |
HEIMDALL_AUTH_TOKEN_EXPIRATION_IN_SECONDS | 3600 (1 hour) |
HEIMDALL_AUTH_TOKEN_SECRET_PREVIOUS | Empty (set only while rotating the signing secret , to a value held to the same rules as the secret) |
HEIMDALL_APPLICATION_TOKEN_EXPIRATION_IN_SECONDS | 600 (10 minutes) |
HEIMDALL_AUTH_MAX_CONCURRENT_PASSWORD_HASHES | 4 (~2.4 GB of Argon2id working set) |
HEIMDALL_EMAIL_VERIFICATION_TOKEN_EXPIRATION_IN_SECONDS | 86400 (24 hours) |
HEIMDALL_PASSWORD_RESET_TOKEN_EXPIRATION_IN_SECONDS | 3600 (1 hour) |
HEIMDALL_ADMIN_EMAIL_COOLDOWN_IN_SECONDS | 300 (5 minutes) — the per-person cooldown on account email: administrator-sent links, anonymous password recovery, and a person’s own resends, setup codes and email-change notices (which are also capped at five per day-long window); 0 turns the cooldown off |
HEIMDALL_LOG_DIRECTORY | logs |
HEIMDALL_METRICS_PORT | 9464 — the Prometheus scrape port; 0 switches metrics off (see Metrics
) |
HEIMDALL_CORS_ALLOWED_ORIGINS | Empty → every cross-origin request is refused. Set it to your front end’s origin (https://app.example.com, comma separated for several) or a browser client cannot call the API |
HEIMDALL_TRUSTED_PROXIES | Empty → X-Forwarded-For is ignored. Behind a reverse proxy, set it to the proxy’s address or network so the rate limiter and the request log see the real caller |
Optional integrations
Email delivery (Mailgun) and Google Sign-In are each configured by their own variables, and each has a documented behaviour when left unconfigured. See Operations for the full table and the reasoning — including why an unconfigured Production deployment refuses to start rather than logging tokens in plaintext.
Apply migrations
The schema is managed with EF Core migrations, applied explicitly. The API never migrates on startup, and refuses to start when migrations are pending — so this step comes before the first run:
python scripts/migrations.py
The script asks which environment file to load (for the connection string), then offers list, create, and apply; apply is the one a deployment needs. See Operations for more, and Contributing for authoring a new migration.
Run
dotnet run --project src/Presentation/ArturRios.Heimdall.WebApi
On the first run against an empty database, the seeder creates the roles and the master system
administrator from HEIMDALL_MASTER_USER_NAME / _EMAIL / _PASSWORD. On later runs, an existing
system admin means those variables are read but unused.
The http launch profile serves on http://localhost:5177; the https profile adds
https://localhost:7235.
Swagger UI is at http://localhost:5177/swagger
, with JWT wired in: press Authorize, paste
the token from a login, and call any endpoint from the browser. It shows each controller’s own
summary and marks which endpoints need a token, because SwaggerConfiguration is applied over it.
That same class produces the document behind the API explorer
, so the two are the
same document — literally: python scripts/openapi.py writes byte-for-byte what a running instance
serves at /swagger/v1/swagger.json. Read the API without running it through the explorer; call it
through Swagger UI or api-client/.
Startup.Build() registers ExceptionMiddleware, then UseSwagger(), then
AuthenticationMiddleware — not both middlewares before Swagger. Authentication does not exempt the
Swagger routes, so registering it first answered 401 to every request for /swagger, index.html
included, which no browser can satisfy. Keeping the exception middleware ahead means a failure inside
Swagger still answers the usual JSON envelope.
Swagger is registered only in the environments the library allows, and in Production it registers nothing at all — so this does not publish the document from a production instance.
First calls
Liveness — anonymous, no database:
curl http://localhost:5177/healthcheck
Log in as the master system administrator:
curl -X POST http://localhost:5177/api/auth/login -H "Content-Type: application/json" -d '{"email":"<master-email>","password":"<master-password>"}'
The response carries token, expiresAt, and emailVerified — the last telling you whether to
prompt the person to confirm their address. Send it as Authorization: Bearer <token> on every
other endpoint — see the API reference
for who may call what.
If the person has active 2FA, login answers requiresTwoFactor: true with a short-lived
challengeToken instead of a full token. Finish the login at POST /api/auth/2fa/verify — see the
two-factor flow
.
Calling the API
api-client/ holds ready-to-send requests for every operation in the OpenAPI document, in two formats — pick whichever your
editor already opens:
| Directory | Format | Opens in |
|---|---|---|
api-client/http/ | JetBrains HTTP Client (.http) | Rider, IntelliJ IDEA, VS Code with the REST Client extension |
api-client/bruno/ | Bruno collection | The Bruno app, or bru run on the command line |
Neither asks you to copy a GUID by hand: a response handler stores the token that login returns, and
each request that creates something stores the new id for the requests that need it. Point the
environment file at your master user’s credentials first — http/http-client.env.json or
bruno/environments/Local.bru.
On an empty database four requests have to run in order before anything else will find its subject:
Login, then Create an administrator, then Create a scope, then Create a User in a
scope. See api-client/README.md
for the whole story, including how to keep real credentials out of git.
To run the Bruno collection without installing anything:
npx @usebruno/cli run Auth --env Local
Next
- Contributing — building, testing, and the branching and release process.
- Architecture — what happens between the controller and the database.
- Development Workflow — how a use case goes from backlog to merged.