Getting Started
Docker in 5 Minutes
Start a fully functional Clavex instance locally using Docker Compose — PostgreSQL, Redis, and the Clavex server, wired together in a single command. By the end you'll have a working OIDC issuer and your first tenant registered.
Prerequisites: Docker ≥ 24 and Docker Compose v2 (
docker compose).
No Go toolchain required. The binary is pre-built in the image.
1
Download docker-compose.yml
The Compose file starts PostgreSQL 16, Redis 7, and the latest Clavex image.
A
config.dev.yaml file with safe defaults is generated automatically
on first run via an entrypoint script.
$ curl -fsSL https://raw.githubusercontent.com/fcraviolatti/clavex/main/docker-compose.yml -o docker-compose.yml
$ curl -fsSL https://raw.githubusercontent.com/fcraviolatti/clavex/main/config.example.yaml -o config.yaml
# Edit config.yaml — set admin.email and a strong jwt_secret (32+ chars)
$ vim config.yaml
Tip: Generate a secure secret with
openssl rand -hex 32 and paste it into
auth.jwt_secret.
2
Start the stack
Compose starts three containers:
postgres, redis, and
clavex. The server runs migrations on startup and is ready in a few seconds.
$ docker compose up -d
[+] Running 3/3
✔ Container clavex-postgres-1 Started
✔ Container clavex-redis-1 Started
✔ Container clavex-server-1 Started
$ curl -s http://localhost:8080/health | jq . { "status": "ok", "db": "ok", "redis": "ok" }
$ curl -s http://localhost:8080/health | jq . { "status": "ok", "db": "ok", "redis": "ok" }
3
Create your first tenant (Organization)
The super-admin token is printed to the server logs on first startup.
Use it to create an Organization — this is your OIDC tenant. The slug
becomes part of the issuer URL:
http://localhost:8080/{slug}.
# Grab the initial superadmin token from the logs
$ export SUPERADMIN_TOKEN=$(docker compose logs server | grep "superadmin-token" | awk '{print $NF}')
# Create organization $ curl -s -X POST http://localhost:8080/api/v1/organizations \ -H "Authorization: Bearer $SUPERADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"Acme Corp","slug":"acme","admin_email":"admin@acme.eu"}' | jq . { "id": "01925f3a-...", "slug": "acme", "name": "Acme Corp" }
# Your OIDC discovery URL is now live $ curl -s http://localhost:8080/acme/.well-known/openid-configuration | jq .issuer "http://localhost:8080/acme"
# Create organization $ curl -s -X POST http://localhost:8080/api/v1/organizations \ -H "Authorization: Bearer $SUPERADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"Acme Corp","slug":"acme","admin_email":"admin@acme.eu"}' | jq . { "id": "01925f3a-...", "slug": "acme", "name": "Acme Corp" }
# Your OIDC discovery URL is now live $ curl -s http://localhost:8080/acme/.well-known/openid-configuration | jq .issuer "http://localhost:8080/acme"
4
Register an OIDC client
Register your application as an OIDC client within the
acme organization.
Public clients (SPAs, mobile) use PKCE only — omit the client_secret.
Confidential clients (backend) receive a generated secret.
$ export ORG_ID="01925f3a-..." # from step 3
$ export ORG_TOKEN="..." # org admin token from the invitation email
$ curl -s -X POST http://localhost:8080/api/v1/organizations/$ORG_ID/clients \ -H "Authorization: Bearer $ORG_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client_name": "My Web App", "redirect_uris": ["http://localhost:3000/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none" }' | jq '{client_id, client_secret}' { "client_id": "clavex_01925f...", "client_secret": null }
$ curl -s -X POST http://localhost:8080/api/v1/organizations/$ORG_ID/clients \ -H "Authorization: Bearer $ORG_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client_name": "My Web App", "redirect_uris": ["http://localhost:3000/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none" }' | jq '{client_id, client_secret}' { "client_id": "clavex_01925f...", "client_secret": null }
5
Test the authorization flow
Open the authorization URL in your browser. Log in with the admin user created
in step 3. After consent, you'll be redirected to
http://localhost:3000/callback?code=....
# Generate PKCE verifier + challenge
$ VERIFIER=$(openssl rand -base64 32 | tr -d '=/+' | cut -c1-43)
$ CHALLENGE=$(echo -n "$VERIFIER" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '=')
# Open in browser $ open "http://localhost:8080/acme/authorize?\ client_id=clavex_01925f...&\ redirect_uri=http://localhost:3000/callback&\ response_type=code&scope=openid+email+profile&\ state=$(openssl rand -hex 8)&\ code_challenge=$CHALLENGE&code_challenge_method=S256"
# After login, exchange the code for tokens $ curl -s -X POST http://localhost:8080/acme/token \ -d "grant_type=authorization_code&code=AUTH_CODE&\ redirect_uri=http://localhost:3000/callback&\ client_id=clavex_01925f...&code_verifier=$VERIFIER" | jq . { "access_token": "eyJ...", "id_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600 }
# Open in browser $ open "http://localhost:8080/acme/authorize?\ client_id=clavex_01925f...&\ redirect_uri=http://localhost:3000/callback&\ response_type=code&scope=openid+email+profile&\ state=$(openssl rand -hex 8)&\ code_challenge=$CHALLENGE&code_challenge_method=S256"
# After login, exchange the code for tokens $ curl -s -X POST http://localhost:8080/acme/token \ -d "grant_type=authorization_code&code=AUTH_CODE&\ redirect_uri=http://localhost:3000/callback&\ client_id=clavex_01925f...&code_verifier=$VERIFIER" | jq . { "access_token": "eyJ...", "id_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600 }
Done! You have a working OIDC flow. The
id_token is a
signed JWT — verify it with the JWKS at
http://localhost:8080/acme/.well-known/jwks.json.