Quickstart: identity service
Quickstart: identity service
Section titled “Quickstart: identity service”This guide gets you from zero to an authenticated JWT in under five minutes. You will bootstrap an owner account, log in to receive a JWT and refresh token, then verify your credentials with a protected endpoint.
Prerequisites
Section titled “Prerequisites”- Docker and
docker composeavailable locally. curlandjqinstalled.- The identity service running:
docker compose up -dfrom the repo root spins up Postgres, PgBouncer, and the identity server onlocalhost:8081.
Step 1 — Bootstrap the first owner
Section titled “Step 1 — Bootstrap the first owner”The bootstrap command creates the first user, organization, and owner membership in one idempotent transaction. Run it once; re-running is a safe no-op.
# from paper-board/identity/docker compose run --rm cli bootstrap \ --email owner@example.com \ --name "Owner" \ --org-name "Acme" \ --org-slug "acme" \ --password-stdin <<< "changeme"The command prints the created user UUID and org UUID. Keep the org UUID — you will need it in later steps.
Step 2 — Log in and receive tokens
Section titled “Step 2 — Log in and receive tokens”curl -s -X POST http://localhost:8081/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"owner@example.com","password":"changeme"}' \ | jq .A successful response looks like:
{ "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "pb_live_rt_..."}Export the access token for subsequent requests:
TOKEN=$(curl -s -X POST http://localhost:8081/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"owner@example.com","password":"changeme"}' \ | jq -r .access_token)Step 3 — Call a protected endpoint
Section titled “Step 3 — Call a protected endpoint”Verify the token works by fetching your user profile:
curl -s http://localhost:8081/v1/users/me \ -H "Authorization: Bearer $TOKEN" \ | jq .Expected response:
{ "id": "<user-uuid>", "email": "owner@example.com", "name": "Owner", "created_at": "2026-05-24T00:00:00Z"}Step 4 — Refresh the token
Section titled “Step 4 — Refresh the token”When the access token expires (default TTL: 3600 s), use the refresh token to rotate:
curl -s -X POST http://localhost:8081/v1/auth/refresh \ -H "Content-Type: application/json" \ -d "{\"refresh_token\":\"$REFRESH\"}" \ | jq .The response is the same shape as /v1/auth/login. The old refresh token is revoked. Using a
consumed refresh token triggers reuse-detection and revokes the entire token family.
Request flow
Section titled “Request flow”sequenceDiagram participant Client participant identity as identity HTTP :8081 participant DB as Postgres (identity schema)
Client->>identity: POST /v1/auth/login {email, password} identity->>DB: LookupUserByEmail DB-->>identity: user row identity->>identity: argon2id verify password identity->>DB: ListOrgMembershipsForUser DB-->>identity: memberships identity->>DB: GetActiveAuthKey DB-->>identity: RS256 private key (encrypted) identity->>identity: decrypt KEK → sign JWT identity->>DB: InsertRefreshToken DB-->>identity: refresh_token row identity-->>Client: 200 {access_token, refresh_token, ...}