Getting started

Prerequisites, configuration, migrations, and running the API locally.

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:

EnvironmentVariablesApp settings
Local (what the launch profiles set)Environments/.env.localSettings/appsettings.Local.json
DevelopmentEnvironments/.env.developmentSettings/appsettings.Development.json
StagingEnvironments/.env.stagingSettings/appsettings.Staging.json
ProductionEnvironments/.env.productionSettings/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.

Required — the API will not start without these

VariableValue
HEIMDALL_DATA_CONNECTIONSTRINGA PostgreSQL connection string
HEIMDALL_DATA_DATABASETYPEPostgreSql
HEIMDALL_AUTH_TOKEN_SECRETThe 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_AUDIENCEThe 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 / _PASSWORDCredentials 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

VariableDefault
HEIMDALL_AUTH_TOKEN_ISSUER / _AUDIENCEEmpty
HEIMDALL_AUTH_TOKEN_EXPIRATION_IN_SECONDS3600 (1 hour)
HEIMDALL_AUTH_TOKEN_SECRET_PREVIOUSEmpty (set only while rotating the signing secret , to a value held to the same rules as the secret)
HEIMDALL_APPLICATION_TOKEN_EXPIRATION_IN_SECONDS600 (10 minutes)
HEIMDALL_AUTH_MAX_CONCURRENT_PASSWORD_HASHES4 (~2.4 GB of Argon2id working set)
HEIMDALL_EMAIL_VERIFICATION_TOKEN_EXPIRATION_IN_SECONDS86400 (24 hours)
HEIMDALL_PASSWORD_RESET_TOKEN_EXPIRATION_IN_SECONDS3600 (1 hour)
HEIMDALL_ADMIN_EMAIL_COOLDOWN_IN_SECONDS300 (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_DIRECTORYlogs
HEIMDALL_METRICS_PORT9464 — the Prometheus scrape port; 0 switches metrics off (see Metrics )
HEIMDALL_CORS_ALLOWED_ORIGINSEmpty → 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_PROXIESEmpty → 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/.

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.

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:

DirectoryFormatOpens in
api-client/http/JetBrains HTTP Client (.http)Rider, IntelliJ IDEA, VS Code with the REST Client extension
api-client/bruno/Bruno collectionThe 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