Skip to content

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.

  • Docker and docker compose available locally.
  • curl and jq installed.
  • The identity service running: docker compose up -d from the repo root spins up Postgres, PgBouncer, and the identity server on localhost:8081.

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.

Terminal window
# 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.

Terminal window
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:

Terminal window
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)

Verify the token works by fetching your user profile:

Terminal window
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"
}

When the access token expires (default TTL: 3600 s), use the refresh token to rotate:

Terminal window
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.

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, ...}