Guides
Practical, step-by-step walkthroughs for the most common Heimdall integration patterns. Each guide includes working code you can adapt to your stack.
SaaS customer onboarding
Set up a complete tenant onboarding flow. Create an App when a customer signs up, seed initial roles, and provision the first admin user.
Steps
- 1Customer signs up through your application
- 2Your backend calls POST /v1/apps to create a new tenant
- 3Create default roles (admin, member, viewer) for the new App
- 4Create the first user with the admin role
- 5Issue an access token and redirect the user to their dashboard
Code example
// 1. Create the tenant
const app = await fetch('/heimdall/v1/apps', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + ADMIN_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: slugify(customerName),
display_name: customerName,
}),
}).then(r => r.json());
// 2. Create default roles
await fetch(`/heimdall/v1/apps/${app.name}/roles`, {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + ADMIN_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'admin',
display_name: 'Administrator',
permissions: [
'billing:read',
'billing:write',
'users:read',
'users:write',
'users:invite',
'projects:read',
'projects:write',
],
}),
}).then(r => r.json());
// 3. Create the founding user
const user = await fetch(`/heimdall/v1/apps/${app.name}/users`, {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + ADMIN_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: customerEmail,
display_name: customerName,
password: temporaryPassword,
role: 'admin',
}),
}).then(r => r.json());Service-to-service authentication
Issue scoped M2M tokens for background jobs, cron tasks, or microservices that need to call your API without a user session.
Steps
- 1Create service credentials for the calling service
- 2Store the client_id and client_secret securely (env vars, secrets manager)
- 3The service requests a token using the client_credentials grant
- 4Include the token in the Authorization header on each request
- 5Rotate credentials periodically by creating new ones and revoking the old
Code example
// Service startup: obtain a token
async function getServiceToken() {
const response = await fetch(
'/heimdall/v1/apps/acme-corp/tokens',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.HEIMDALL_CLIENT_ID,
client_secret: process.env.HEIMDALL_CLIENT_SECRET,
}),
}
);
const { access_token, expires_in } = await response.json();
// Cache the token, refresh before expiry
return { token: access_token, expiresIn: expires_in };
}
// Use the token for downstream calls
const { token } = await getServiceToken();
await fetch('/your-api/billing/invoices', {
headers: { 'Authorization': 'Bearer ' + token },
});Team invite flow
Let existing users invite new team members into their workspace. Heimdall handles the invite token, expiration, and user creation on acceptance.
Steps
- 1An admin user triggers an invite from your UI
- 2Your backend calls POST /v1/apps/:app/invites with the target email and role
- 3Heimdall returns an invite_url and token
- 4You send the invite link to the invitee (email, Slack, etc.)
- 5The invitee clicks the link and completes a signup form
- 6Your backend calls POST /v1/invites/:token/accept with the user details
- 7Heimdall creates the user with the specified role and returns a token
Code example
// Admin invites a team member
const invite = await fetch('/heimdall/v1/apps/acme-corp/invites', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + adminToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'bob@contractor.io',
role: 'editor',
expires_in_hours: 72,
max_uses: 1,
}),
}).then(r => r.json());
// Send the invite (your responsibility)
await sendEmail({
to: 'bob@contractor.io',
subject: 'You have been invited to Acme Corp',
body: `Join the team: ${invite.invite_url}`,
});
// Invitee accepts (from your signup page)
const result = await fetch(
`/heimdall/v1/invites/${invite.token}/accept`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
display_name: 'Bob Martinez',
password: userChosenPassword,
}),
}
).then(r => r.json());
// result contains the new user and an access tokenLocal token verification with JWKS
Verify Heimdall tokens locally in your API middleware without making a network call on every request. This reduces latency and removes Heimdall as a runtime dependency for authorization checks.
Steps
- 1Fetch the JWKS keys from the well-known endpoint on application startup
- 2Cache the keys and refresh them periodically (every 6-12 hours)
- 3On each incoming request, extract the Bearer token from the Authorization header
- 4Verify the JWT signature using the cached public keys
- 5Read the app_id, user_id, roles, and permissions from the token claims
- 6Check the required permission against the permissions array in the token
Code example
import * as jose from 'jose';
// Fetch and cache JWKS on startup
const JWKS = jose.createRemoteJWKSet(
new URL('https://api.productcraft.co/heimdall/v1/.well-known/jwks.json')
);
// Middleware: verify token and check permission
async function authorize(req, requiredPermission) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) throw new Error('Missing token');
const { payload } = await jose.jwtVerify(token, JWKS, {
issuer: 'https://api.productcraft.co/heimdall',
});
if (!payload.permissions.includes(requiredPermission)) {
throw new Error('Insufficient permissions');
}
return {
userId: payload.sub,
appId: payload.app_id,
roles: payload.roles,
permissions: payload.permissions,
};
}
// Usage in a route handler
app.get('/api/invoices', async (req, res) => {
const auth = await authorize(req, 'billing:read');
// auth.userId, auth.appId are available
const invoices = await getInvoices(auth.appId);
res.json(invoices);
});Reacting to Heimdall events
Subscribe to webhook events to keep your system in sync with Heimdall. Get notified when users are created, roles change, or invites are accepted.
Steps
- 1Register a webhook endpoint in your App settings
- 2Heimdall sends signed POST requests to your endpoint for each event
- 3Verify the webhook signature using your App's webhook secret
- 4Process the event payload and acknowledge with a 200 response
- 5If your endpoint is unreachable, Heimdall retries with exponential backoff
Code example
// Webhook handler (Express example)
app.post('/webhooks/heimdall', async (req, res) => {
const signature = req.headers['x-heimdall-signature'];
const isValid = verifySignature(
req.body,
signature,
process.env.HEIMDALL_WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.body;
switch (event.type) {
case 'user.created':
await provisionUserResources(event.data.user_id, event.data.app_id);
break;
case 'user.deactivated':
await cleanupUserSessions(event.data.user_id);
break;
case 'role.assigned':
await syncPermissionsCache(event.data.user_id);
break;
case 'invite.accepted':
await sendWelcomeEmail(event.data.user_id);
break;
}
res.status(200).json({ received: true });
});