← Heimdall Docs

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

  1. 1Customer signs up through your application
  2. 2Your backend calls POST /v1/apps to create a new tenant
  3. 3Create default roles (admin, member, viewer) for the new App
  4. 4Create the first user with the admin role
  5. 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

  1. 1Create service credentials for the calling service
  2. 2Store the client_id and client_secret securely (env vars, secrets manager)
  3. 3The service requests a token using the client_credentials grant
  4. 4Include the token in the Authorization header on each request
  5. 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

  1. 1An admin user triggers an invite from your UI
  2. 2Your backend calls POST /v1/apps/:app/invites with the target email and role
  3. 3Heimdall returns an invite_url and token
  4. 4You send the invite link to the invitee (email, Slack, etc.)
  5. 5The invitee clicks the link and completes a signup form
  6. 6Your backend calls POST /v1/invites/:token/accept with the user details
  7. 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 token

Local 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

  1. 1Fetch the JWKS keys from the well-known endpoint on application startup
  2. 2Cache the keys and refresh them periodically (every 6-12 hours)
  3. 3On each incoming request, extract the Bearer token from the Authorization header
  4. 4Verify the JWT signature using the cached public keys
  5. 5Read the app_id, user_id, roles, and permissions from the token claims
  6. 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

  1. 1Register a webhook endpoint in your App settings
  2. 2Heimdall sends signed POST requests to your endpoint for each event
  3. 3Verify the webhook signature using your App's webhook secret
  4. 4Process the event payload and acknowledge with a 200 response
  5. 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 });
});