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
- 1Customer signs up through your application via POST /v1/auth/signup
- 2Your backend creates an App via POST /v1/apps — the customer becomes the owner automatically
- 3System roles (owner, admin, member) are provisioned automatically with default permissions
- 4Optionally create custom roles via POST /v1/apps/:appId/roles
- 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
- 1Create service credentials via POST /v1/apps/:appId/credentials
- 2Store the client_id and client_secret securely (env vars, secrets manager)
- 3Set permission scopes via PUT /v1/apps/:appId/credentials/:clientId/scopes
- 4The service requests a token via POST /v1/oauth/token
- 5Include the token in the Authorization header on each request
- 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
- 1An admin user triggers an invite from your UI
- 2Your backend calls POST /v1/apps/:appId/invites with the target role name and optional email
- 3Heimdall returns an invite code
- 4You deliver the code to the invitee (email, Slack, etc.)
- 5The invitee signs up via POST /v1/auth/signup (if they don't already have an account)
- 6The invitee calls POST /v1/invites/accept with the code and their auth token
- 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 joinedLocal 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 GET /v1/.well-known/jwks.json on application startup
- 2Cache the keys and refresh them periodically (every 1-6 hours)
- 3On each incoming request, extract the Bearer token from the Authorization header
- 4Verify the JWT signature using the cached public keys
- 5Read the user identity from the token claims
- 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
- 1Extract the user's token from the incoming request
- 2Call GET /v1/introspect/apps/:appId with the token
- 3Check if isMember is true and the required permission is in the permissions array
- 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
);