← 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 user as the owner.

Steps

  1. 1Customer signs up through your application via POST /v1/auth/signup
  2. 2Your backend creates an App via POST /v1/apps — the customer becomes the owner automatically
  3. 3System roles (owner, admin, member) are provisioned automatically with default permissions
  4. 4Optionally create custom roles via POST /v1/apps/:appId/roles
  5. 5The customer is ready to invite their team members

Code example

// 1. Customer signs up
const auth = await fetch('/api/v1/auth/signup', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: customerEmail,
    username: slugify(customerName),
    password: customerPassword,
    displayName: customerName,
  }),
}).then(r => r.json());

// 2. Create an App (customer becomes owner automatically)
const app = await fetch('/api/v1/apps', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + auth.access_token,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    slug: slugify(customerName),
    displayName: customerName,
  }),
}).then(r => r.json());

// 3. System roles are already created (owner, admin, member)
// The customer has the owner role with all permissions

// 4. Optionally create a custom role
await fetch(`/api/v1/apps/${app.slug}/roles`, {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + auth.access_token,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'viewer',
    description: 'Read-only access',
  }),
}).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 via POST /v1/apps/:appId/credentials
  2. 2Store the client_id and client_secret securely (env vars, secrets manager)
  3. 3Set permission scopes via PUT /v1/apps/:appId/credentials/:clientId/scopes
  4. 4The service requests a token via POST /v1/oauth/token
  5. 5Include the token in the Authorization header on each request
  6. 6Rotate credentials periodically via POST /v1/apps/:appId/credentials/:clientId/rotate

Code example

// Service startup: obtain a token
async function getServiceToken() {
  const response = await fetch(
    '/api/v1/oauth/token',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        clientId: process.env.HEIMDALL_CLIENT_ID,
        clientSecret: process.env.HEIMDALL_CLIENT_SECRET,
      }),
    }
  );
  const { access_token, expires_in, scope } = await response.json();

  // Cache the token, refresh before expiry
  return { token: access_token, expiresIn: expires_in, scope };
}

// 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 code, expiration, and membership creation on acceptance.

Steps

  1. 1An admin user triggers an invite from your UI
  2. 2Your backend calls POST /v1/apps/:appId/invites with the target role name and optional email
  3. 3Heimdall returns an invite code
  4. 4You deliver the code to the invitee (email, Slack, etc.)
  5. 5The invitee signs up via POST /v1/auth/signup (if they don't already have an account)
  6. 6The invitee calls POST /v1/invites/accept with the code and their auth token
  7. 7Heimdall adds the user to the App with the specified role

Code example

// Admin invites a team member
const invite = await fetch('/api/v1/apps/acme-corp/invites', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + adminToken,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    role: 'admin',
    email: 'bob@contractor.io',
    expiresInHours: 72,
    maxUses: 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 with code: ${invite.code}`,
});

// Invitee signs up (if needed)
const inviteeAuth = await fetch('/api/v1/auth/signup', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: 'bob@contractor.io',
    username: 'bob.martinez',
    password: userChosenPassword,
    displayName: 'Bob Martinez',
  }),
}).then(r => r.json());

// Invitee accepts the invite
const result = await fetch('/api/v1/invites/accept', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + inviteeAuth.access_token,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ code: invite.code }),
}).then(r => r.json());

// result is the tenant the invitee just joined

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 GET /v1/.well-known/jwks.json on application startup
  2. 2Cache the keys and refresh them periodically (every 1-6 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 user identity from the token claims
  6. 6Optionally call the introspection endpoint to check App-specific permissions

Code example

import * as jose from 'jose';

// Fetch and cache JWKS on startup
const JWKS = jose.createRemoteJWKSet(
  new URL('https://heimdall.productcraft.co/api/v1/.well-known/jwks.json')
);

// Middleware: verify token
async function verifyToken(req) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) throw new Error('Missing token');

  const { payload } = await jose.jwtVerify(token, JWKS, {
    issuer: 'https://heimdall.productcraft.co/api',
  });

  return {
    userId: payload.sub,
    email: payload.email,
    name: payload.name,
  };
}

// Check App-specific permissions via introspection
async function checkPermission(token, tenantId, requiredPermission) {
  const response = await fetch(
    `https://heimdall.productcraft.co/api/v1/introspect/apps/${tenantId}`,
    { headers: { 'Authorization': 'Bearer ' + token } }
  );
  const { isMember, permissions } = await response.json();

  if (!isMember || !permissions.includes(requiredPermission)) {
    throw new Error('Insufficient permissions');
  }
}

// Usage in a route handler
app.get('/api/invoices', async (req, res) => {
  const user = await verifyToken(req);
  await checkPermission(
    req.headers.authorization.replace('Bearer ', ''),
    req.params.tenantId,
    'audit.read'
  );
  const invoices = await getInvoices(req.params.tenantId);
  res.json(invoices);
});

Checking permissions in your API

Use the introspection endpoint to verify whether a user has a specific permission within an App before allowing an action.

Steps

  1. 1Extract the user's token from the incoming request
  2. 2Call GET /v1/introspect/apps/:appId with the token
  3. 3Check if isMember is true and the required permission is in the permissions array
  4. 4Allow or deny the action based on the result

Code example

// Express middleware example
function requirePermission(tenantIdParam, permission) {
  return async (req, res, next) => {
    const token = req.headers.authorization?.replace('Bearer ', '');
    const tenantId = req.params[tenantIdParam];

    if (!token || !tenantId) {
      return res.status(401).json({ error: 'Unauthorized' });
    }

    try {
      const response = await fetch(
        `${HEIMDALL_URL}/v1/introspect/apps/${tenantId}`,
        { headers: { 'Authorization': 'Bearer ' + token } }
      );
      const { isMember, permissions } = await response.json();

      if (!isMember || !permissions.includes(permission)) {
        return res.status(403).json({ error: 'Forbidden' });
      }

      next();
    } catch {
      return res.status(401).json({ error: 'Invalid token' });
    }
  };
}

// Usage
app.delete(
  '/api/:tenantId/projects/:projectId',
  requirePermission('tenantId', 'tenant.delete'),
  deleteProjectHandler
);