Heimdall guides
02 · Quickstart

Wire signup + signin into your product.

Provision an app from the console, hit signup, signin, and /me from a test client. By the end you have one working end-user authenticating against your app, with a JWT you can verify in your own backend.


Step 1

Create an app in the console

From console.productcraft.co/heimdall, hit Create app. Pick a slug — it appears in every Consumer-API URL for the rest of this guide.

The slug is permanent: it's baked into every token issued for the app and showing up in URL paths customer apps render. Change the display name freely, but pick a slug you can live with. For this guide we'll use acme.

After create, you're the app owner. The console shows three system roles (owner, admin, member) seeded with their default permissions. You can customise these later — Step 5 covers that.


Step 2

Check the auth config

Open Settings → Auth config in the console. The defaults are sensible — leave them unless your product needs different behaviour.

The main toggles:

  • signup_enabled (default true) — if false, POST /auth/signup returns 403. Useful for invite-only products.
  • signin_enabled (default true) — emergency switch to lock out new signins without revoking issued tokens.
  • signup_requires_pak (default false) — if true, signup needs a PAK with heimdall.signup.create. Use this when only your backend should create users, never the public form.
  • password_min_length (default 8) — plus optional password_require_uppercase,password_require_number,password_require_symbol.
  • access_token_ttl_seconds (default 3600) and refresh_token_ttl_seconds (default 30 days).
  • enforce_app_permissions (default false) — when off, any valid token can call any Consumer-API route. When on, the@RequireAppPermission gates kick in. Keep this off until you've modelled your roles.

Step 3

Sign up your first user

Public surface. No bearer needed. The response includes an access token + refresh token ready to use. Email is the primary contact and starts unverified — that's fine for the first call; Chapter 7 covers the verification flow.

POST /acme/v1/auth/signup
curl https://api.heimdall.productcraft.co/acme/v1/auth/signup \
  -H 'content-type: application/json' \
  -d '{
    "email": "ada@example.com",
    "username": "ada",
    "password": "CorrectHorseBatteryStaple",
    "display_name": "Ada Lovelace"
  }'

Response:

{
  "access_token":  "eyJhbGc... (1h TTL)",
  "refresh_token": "eyJhbGc... (30d TTL)",
  "token_type": "Bearer",
  "expires_in": 3600
}

Step 4

Sign in an existing user

Same response shape as signup. identifier accepts either the username or any verified email contact — unverified emails are explicitly not accepted to avoid account-takeover via email squatting.

POST /acme/v1/auth/signin
curl https://api.heimdall.productcraft.co/acme/v1/auth/signin \
  -H 'content-type: application/json' \
  -d '{
    "identifier": "ada",
    "password": "CorrectHorseBatteryStaple"
  }'

Failure modes — 401 { statusCode:401, message:"Invalid credentials" } for wrong password, unknown identifier, suspended account, or signin-disabled app. Heimdall does not differentiate between those four to avoid a username-existence oracle.


Step 5

Read the user's profile

With the access token from step 3 or 4, hit /me. This is the canonical 'who am I' endpoint.

GET /acme/v1/me
curl https://api.heimdall.productcraft.co/acme/v1/me \
  -H 'authorization: Bearer eyJhbGc...'
{
  "id": "648616c8-...",
  "username": "ada",
  "displayName": "Ada Lovelace",
  "role": "member",
  "joinedAt": "2026-05-11T11:39:37Z",
  "createdAt": "2026-05-11T11:39:35Z",
  "email": "ada@example.com",
  "emailVerifiedAt": null
}

role is the user's role name on this app — by default signup_default_role (which defaults to member). To get the flat permission set bound to that role, hit GET /me/permissions — that resolves through Heimdall's 60-second LRU so role changes propagate within a minute.


Step 6

Refresh the access token

Access tokens expire after 1h. Use the refresh token to mint a fresh pair without re-asking for credentials. The refresh token is rotated on every call — the previous one is revoked, and re-using a previous refresh token within the rotation grace window triggers session revocation (a stolen token + the legitimate user's refresh collide, and we destroy both).

POST /acme/v1/auth/refresh
curl https://api.heimdall.productcraft.co/acme/v1/auth/refresh \
  -H 'content-type: application/json' \
  -d '{ "refresh_token": "eyJhbGc... (the previous refresh)" }'

Step 7

Sign out

Two options: revoke the refresh token (kills the session, access token still valid until TTL) or call /me/sessions/:id (server-side revoke, immediately invalidates the bearer).

POST /acme/v1/auth/logout (the simple path)
curl https://api.heimdall.productcraft.co/acme/v1/auth/logout \
  -H 'content-type: application/json' \
  -d '{ "refresh_token": "eyJhbGc..." }'

What's next

You have a working Heimdall integration

From here:

  • Chapter 3 — bind permissions to roles so your routes can say “only users with billing.read may call this.”
  • Chapter 6 — verify the JWT in your own backend, not by calling /me on every request.
  • Chapter 7 — wire up email verification so unverified emails don't accumulate forever.
  • The full API reference at /docs/heimdall/api-reference has every endpoint, including the ones we didn't touch here (contact management, session list, activity log, tenant switching).