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(defaulttrue) — if false,POST /auth/signupreturns 403. Useful for invite-only products.signin_enabled(defaulttrue) — emergency switch to lock out new signins without revoking issued tokens.signup_requires_pak(defaultfalse) — if true, signup needs a PAK withheimdall.signup.create. Use this when only your backend should create users, never the public form.password_min_length(default8) — plus optionalpassword_require_uppercase,password_require_number,password_require_symbol.access_token_ttl_seconds(default3600) andrefresh_token_ttl_seconds(default30 days).enforce_app_permissions(defaultfalse) — when off, any valid token can call any Consumer-API route. When on, the@RequireAppPermissiongates 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.
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.
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.
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).
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).
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.readmay 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).