id
Accounts, passkeys, teams, projects, roles and tokens for the whole instance, and the OpenID Connect
provider every service signs in through. Host: id.<d>. Binary: idd. Stage 2.
Concepts
- User: name (DNS label, unique), display name, e-mail (optional, verified), avatar, instance role
(
superuser | user), status (active, disabled), created by.
- Passkey: WebAuthn credential, RP ID =
<d>, name, created, last used. A user has one or more.
- Sign-in link: one-time, 72 h, issued by a superuser (onboarding, lost device; sent to the user's
confirmed e-mail if asked) or by
xvii-node recover <user> on the server; or 30 min, asked for on the
sign-in page by name or address and e-mailed to the account's confirmed address (the answer is the same
either way; ten an hour per IP, one per two minutes per account). Redeeming adds a passkey.
- E-mail: optional; it counts once confirmed with a six-digit code sent to it (15 min, five tries,
a new address is unconfirmed again). id sends through the instance's SMTP (settings
SMTP_URL,
MAIL_FROM); without them the e-mail features are off (/api/v1/meta mail).
- Session: browser session at
id (30 days sliding), list and revoke per device.
- Team: name, slug, members with roles (
owner, admin, developer, viewer), quotas (owned by
the services, shown here), personal flag. Every user gets a personal team.
- Invitation: to a team, by user name or e-mail, with a role; accepted by the invitee.
- Invitation link: a one-time link a team admin hands to someone with no account yet (or when the
instance has no mail): admin, developer or viewer, a week; its holder picks a name, sets a passkey
and is signed in as a member.
- Project: belongs to a team; name, slug, description, repository links (GitLab, GitHub);
environments. Project overrides limit or extend members' roles (ADR 0005).
- Environment: belongs to a project;
production and preview exist by default; protection flag.
- API token:
xv_…, stored as SHA-256; owner (user or team), scope (read, deploy, write),
optional services, project, expiry; last used.
- Trust rule: lets CI from a repository (GitLab
id_tokens, GitHub Actions OIDC) exchange its job
token for a short-lived token with a scope and project, optionally only for some branches.
- OIDC client: every service registers itself on install (client id, redirect URIs, first-party);
third-party clients can be added by superusers.
- Plan (ADR 0013): an instance-wide bundle of limits (
pages.deploys, apps.memory…), one may be
the default for personal teams and one for other teams. A team gets a plan (or the default) plus
values of its own; a project gets caps inside them. Services declare their limit keys, read the
effective values with their client credentials and report usage; the team's and the project's Limits
tab shows both.
- Trust rule: lets CI jobs of a repository (GitLab
id_tokens, GitHub Actions OIDC, any OIDC issuer)
trade their job token for a short-lived token of the team: issuer, repository, branch globs,
protected refs only, scope, role, lifetime. Kept per team, usually per project (the project's CI tab).
- Grant: a shared resource from one team (or the instance) to another, with a quota. Stored in the
owning service;
id only lists them for the team page.
OIDC
- Discovery at
/.well-known/openid-configuration, keys at /.well-known/jwks.json (Ed25519, rotated,
two keys published during rotation).
- Authorization code + PKCE for panels; device authorization grant (RFC 8628) for the CLI; client
credentials for services; token exchange (RFC 8693) for CI job tokens; refresh tokens with rotation.
- Access tokens: JWT, 15 minutes, claims
sub, name, role, teams ({team id: role}), scope,
aud (service). ID tokens for panels. Introspection endpoint for opaque API tokens.
- First-party clients skip the consent screen.
API (highlights)
GET /api/v1/me who am I, teams, roles
POST /api/v1/me/email/code | /me/email/verify confirm the account's e-mail with a code
POST /api/v1/auth/email-link a sign-in link to a confirmed address {login, lang}
GET /api/v1/teams | POST /teams | PATCH /teams/{id} | DELETE /teams/{id}
GET /api/v1/teams/{id}/members | POST …/invitations | PATCH …/members/{user} | DELETE …
GET /api/v1/teams/{id}/invitation-links | POST … | DELETE …/{inv} links for people with no account
GET|POST /api/v1/auth/invitation-links/{secret}[/begin|/finish] the public side of a link
GET /api/v1/projects?team= | POST /projects | PATCH /projects/{id} | DELETE /projects/{id}
GET /api/v1/projects/{id}/environments | POST … | PATCH …/{env}
GET /api/v1/tokens | POST /tokens | DELETE /tokens/{id}
GET /api/v1/trust-rules?team=|project= | POST … | DELETE …/{id}
GET /api/v1/plans | POST | PATCH /plans/{id} | DELETE (superusers change)
GET /api/v1/teams/{id}/limits | PUT (superusers) · /projects/{id}/limits | PUT (team admins)
GET /api/v1/limits · POST /api/v1/usage (services, client credentials)
GET /api/v1/directory teams, projects, memberships, superusers (services, client credentials)
POST /oauth/token grant_type=urn:ietf:params:oauth:grant-type:token-exchange (CI job tokens)
GET /api/v1/users | POST /users | PATCH /users/{id} | POST /users/{id}/sign-in-link (superuser)
GET /api/v1/sessions | DELETE /sessions/{id} | GET /api/v1/passkeys | PATCH | DELETE
Panel
Built like pages: numbers on top, full-width tables with search and filters, a side sheet per row.
Sign-in (passkey button, nothing else), onboarding from a link; Projects (the start page; a sheet with
overview, environments, limits, CI, settings), Teams (a sheet with members and invitations, projects, limits, team
tokens, settings), API tokens (own and team tokens in one table), Activity; Account (profile, passkeys,
browsers, CLI sign-ins), Connect (CLI install and sign-in, GitLab CI snippet, handy commands);
Users and Plans (superusers); the CLI approval page (/device). Later: quotas per service, grants, OIDC clients.
CLI
xvii login | logout | whoami, xvii teams …, xvii projects …, xvii tokens …, xvii users ….
Events
user.created, user.disabled, team.member_added, team.member_removed, project.created,
token.created, token.revoked, passkey.added, sign_in.
Settings
XVII_ID_DOMAIN (base domain, the passkeys' RP ID), XVII_ID_RP_ID (only for local development:
browsers refuse localhost for id.localhost), XVII_ID_FIRST_USER (the superuser created on an empty
database; may be a reserved name), XVII_ID_CLIENTS_DIR, XVII_ID_DATABASE_URL, XVII_ID_PUBLIC_URL.
The activity log records how every change was made (details.via: panel, cli, token and its name).
Migration from pages
pages' users are not carried over: the author re-enrolled at id with a passkey. Their API tokens are:
idd import-pages-tokens USER accounts.json stores them under USER with the same hash (pages v1 and id
both keep hex SHA-256), limited to the pages service, full becoming write, so CI keeps deploying with
the xp_… secrets it has. id and kit accept xp_ next to xv_ as API tokens. Any token may read
/api/v1/me at id, so services can name the teams of callers whose tokens are limited to them.
Later
GitHub and GitLab as linked identities (for repository access in
apps and pages), optionally as sign-in methods if an instance enables them; SCIM for bigger teams.