Core Concepts
Heimdall is built around six primitives. Understanding how they relate to each other is the key to a clean integration.
Apps
An App is Heimdall's unit of tenancy. If you are building a SaaS product, each of your customers is an App. If you are building a platform, each workspace or organization is an App.
All data in Heimdall is scoped to an App. Roles, permissions, tokens, and audit logs belong to exactly one App and are invisible to all others. This isolation is enforced automatically on every query.
When you create an App, three system roles are provisioned automatically: owner, admin, and member. The creator is assigned the owner role with full permissions.
Key properties
id- Unique identifier (UUID)slug- URL-safe identifier, globally uniquedisplay_name- Human-readable labelstatus- active, suspended, or archivedmetadata- Arbitrary JSON for your application-specific data
Users
A User represents a person who can authenticate with Heimdall. Users register once and can be members of multiple Apps. Within each App, a user holds a specific role that determines their permissions.
Users authenticate with email/username and password, or through social login providers (Google, GitHub). A single person can belong to many Apps — for example, a developer who belongs to both their own workspace and a client's workspace.
Key properties
id- Unique identifier (UUID)email- Primary email addressusername- Unique usernamedisplay_name- Human-readable labelstatus- active, suspended, or deactivatedmetadata- Arbitrary JSON for your application-specific data
Roles
A Role is a named collection of permissions within an App. Instead of assigning individual permissions to each user, you define roles (like "admin", "editor", "viewer") and assign those roles to users.
Roles are App-scoped. An "admin" role in one App is completely independent from an "admin" role in another. Each user holds exactly one role per App, and their effective permissions are the permissions of that role.
Every App comes with three system roles that cannot be deleted:
- owner — full permissions on all resources
- admin — management permissions without destructive capabilities
- member — read-only access to basic information
You can create additional custom roles with any combination of the available permissions.
Permissions
Permissions are strings that represent specific actions. Heimdall uses a dot-separated convention: resource.action. All permissions are scoped to the App in which the role carrying them is assigned.
Available permissions
user.create user.read user.update user.delete user.list
role.create role.read role.update role.delete role.assign role.revoke
tenant.read tenant.update tenant.delete
invite.create invite.read invite.revoke
m2m.create m2m.read m2m.update m2m.delete m2m.rotate_secret
audit.readAssign permissions to roles, then assign roles to users. Heimdall checks permissions on every API call that modifies App-scoped data.
Tokens
Heimdall issues JWTs signed with RS256. Every token carries claims about the authenticated entity (user or service).
There are two types of authentication:
- User tokens — issued via signup, signin, or social login. Include the user's identity.
- M2M tokens — issued via client credentials grant. Include the client's scoped permissions.
Access tokens expire after 1 hour by default. Refresh tokens are valid for 30 days. Verify tokens locally using the JWKS endpoint or via the introspection API.
JWKS endpoint
GET /v1/.well-known/jwks.jsonCache the keys locally and verify tokens without a network round-trip. The endpoint returns a Cache-Control header with a 1-hour max-age.
Audit Logs
Every state-changing operation in Heimdall produces an audit log entry. This includes tenant updates, role assignments, permission changes, invite generation, and M2M client management.
Audit logs are immutable, App-scoped, and queryable through a dedicated API endpoint. Each entry records the actor, action, affected resource, and client IP address.
Example audit entry
{
"id": "...",
"tenant_id": "...",
"actor_id": "...",
"actor_type": "user",
"action": "role.permissions_changed",
"resource": "role",
"resource_id": "...",
"metadata": {},
"ip": "203.0.113.42",
"created_at": "2026-02-20T10:15:00Z"
}Invites
Invites let existing users bring new users into an App. When you create an invite, Heimdall generates a unique code with configurable expiration and a maximum number of uses. You control which role the invitee receives on acceptance.
Heimdall returns the invite code and leaves delivery to you. Send invites through email, Slack, SMS, or whatever channel your users prefer. The invitee must have a Heimdall account (signed up via the auth endpoints) before accepting the invite.
Invite lifecycle
- 1. An admin creates an invite with a target role and optional expiry
- 2. Heimdall returns an invite code
- 3. You deliver the code to the invitee
- 4. The invitee signs up (if they haven't already)
- 5. The invitee calls the accept endpoint with the code
- 6. Heimdall adds the user to the App with the specified role
How it all fits together
User (registers once, belongs to many Apps)
└── App (tenant / workspace / organization)
├── Members
│ └── each holds one Role
├── Roles
│ └── contain Permissions
├── Service Credentials (M2M)
│ └── scoped Permissions
├── Invites
└── Audit Logs