Home / Docs / Identity & collaborators
Identity & collaborators
Yggdrasil ships with identity and workforce built in. You don't have to bolt on an external IdP just to know who's who inside the platform: the core already models the collaborators on your internal team, the teams they form and the grants that say what each team can operate. Everything versioned, auditable and drivable from the CLI.
The model
Three aggregates make up identity in the core engine. They live in the core's Postgres and are the subject of every authorization decision.
slug, display_name, primary_email, a lifecycle status, an optional manager_id, a primary_team_id and structured data pockets: employment_data, personal_data, traits and third_party_identities. Every successful write bumps a version (optimistic locking).slug, name, type, status, a canonical email (which integrations can use to provision external groups) and a parent_team_id — teams form a hierarchy. The person↔team link is a TeamMembership, with a role, a validity window (starts_at/ends_at) and a source.integration_instance_namespace + integration_instance_name + action_name; action_name: "*" is a wildcard). This is where authorization is born: a user inherits the grants of their active teams.A collaborator's effective permission isn't a field on their record — it's derived. The core resolves the yggdrasil:* actions a person can run from the team_grants of their active teams over Yggdrasil's own management instance. A wildcard grant (or traits.yggdrasil_admin = true) means full access.
Session & MFA
Login trades credentials for a session token. You send an identifier (the collaborator's slug or email) and the password; the core validates against the local password (Argon2id) and returns a token. A session is a persisted row with expires_at, last_seen_at and revocation state — you can list and end individual sessions (that laptop left behind at the conference).
For accounts with MFA enabled, the first login step responds with mfa_required and the list of factors. You then resend including the second factor — a TOTP code from your authenticator, or a recovery code when you don't have the app at hand. WebAuthn is also supported as a factor. From the CLI:
$ yggdrasil login --server https://ygg.internal:9080 --username ana.silva
# the account has MFA: the core responds mfa_required (factors: totp)
$ yggdrasil login --server https://ygg.internal:9080 --username ana.silva --totp 492013
✓ logged in as ana.silva → context "ygg-internal" saved to ~/.yggdrasil/config.yaml
# no authenticator on hand? use a recovery code:
$ yggdrasil login --server https://ygg.internal:9080 --username ana.silva --recovery-code 9f3a-2bd1-77ce
In an interactive terminal you can omit --server, --username and the code: the CLI prompts for them. Login persists a named context in ~/.yggdrasil/config.yaml (kubectl-style), and subsequent commands reuse that token.
csrf_token derived deterministically from the session id; surfaces mirror that value in the X-CSRF-Token header on every POST/PUT/PATCH/DELETE. The middleware recomputes the HMAC on the server and rejects mismatches — defense in depth on top of SameSite=Strict cookies.Auth providers
Beyond the local password, the core speaks OAuth/OIDC with external providers (your Google Workspace, a corporate IdP, GitHub). Each provider is a core-owned configuration: name, type (e.g. oidc), issuer/authorize/token URLs, a client_id and a client_secret_ref — you reference the secret, never inline it. Fields like auto_link_by_email and the *_field entries tell the core how to map the remote profile onto a collaborator.
Manage providers from the CLI, like any other declarative resource:
$ yggdrasil auth provider list
workspace-google oidc active
$ yggdrasil auth provider apply -f provider-google.yaml
✓ provider "workspace-google" applied
login URL: https://ygg.internal:9080/api/v1/auth/third-party/start/workspace-google
The provider manifest is straightforward — just the fields the core knows about:
name: "workspace-google"
type: "oidc"
status: "active"
display_name: "Google Workspace"
issuer_url: "https://accounts.google.com"
client_id: "1234.apps.googleusercontent.com"
client_secret_ref: "aws-sm://yggdrasil/google-oidc" # a reference, never the secret
scopes: ["openid", "email", "profile"]
auto_link_by_email: true
email_field: "email"
Once the provider is applied, the third-party flow becomes available at /api/v1/auth/third-party/start/<name> — the CLI prints that URL for you to check on the spot. With auto_link_by_email, an external login automatically matches the collaborator with the same email.
Collaborator lifecycle
People join, change teams, take leave, leave — and sometimes come back. yggdrasil collaborator covers that whole lifecycle with explicit verbs, each recording an auditable event in the collaborator's history.
| Verb | What it does |
|---|---|
create | Creates the record — --slug and --display-name required; optional --email, --role, --manager, --team, --start-date. |
get · list | Reads one collaborator (by id or slug) or lists all (list --status <s>). |
update | Patches --display-name, --email or --status. |
offboard | Offboards — --reason <voluntary|involuntary|contract-end|deceased>, with --end-date/--notice-days. |
suspend · unsuspend | Suspends and reactivates access temporarily. |
re-onboard | Re-admits someone who came back (--start-date, --role). |
role-change | Changes the role (--new-role). |
team-add · team-remove | Joins/leaves a team (--team, optional --role-in-team). |
manager-change | Reassigns the manager (--new-manager, empty to clear). |
attribute-set | Writes a typed trait (--key, --value, --type string|number|bool|json). |
absence-start · absence-end | Records an absence (--type vacation|leave-medical|leave-parental|leave-sabbatical). |
lifecycle-events | Reads the collaborator's auditable history (--type, --limit). |
provider-state | Shows how external providers are materializing that identity. |
A typical onboarding chains a few commands — and the rest (provisioning on external systems via integrations) reacts to the state:
$ yggdrasil collaborator create --slug ana.silva --display-name "Ana Silva" \
--email ana@internal --role platform-engineer --team team-infra
$ yggdrasil collaborator team-add ana.silva --team team-oncall --role-in-team responder
$ yggdrasil collaborator lifecycle-events ana.silva --limit 5
Identity is a manifest too
Teams and grants don't only live in the imperative CLI: they're manifest kinds (team, team_grant, team_role_binding), so you declare, version and reconcile them like any other resource. It's GitOps for your own workforce.
team/team_grant model, and Workflows & runs for how an identity change triggers provisioning.