Linux + PostgreSQL
Run oidc-exchange on Linux with PostgreSQL for persistent user and session storage. To reduce session lookup time, you can store sessions separately in Valkey or Redis.
A runnable example is in examples/linux-postgres/.
When to use this
Section titled “When to use this”- You already run PostgreSQL and want one relational database for persistent data.
- You need atomic, consistent, isolated, and durable (ACID) transactions for user records.
- You need to query user data directly with SQL tools.
- You want the option to use Valkey or Redis for session lookups.
Architecture
Section titled “Architecture” ┌──────────────┐ │ nginx │ │ (TLS term) │ └──────┬───────┘ │ ┌──────┴───────┐ │oidc-exchange │ └──┬───────┬───┘ │ │ ┌────────┴──┐ ┌─┴──────────┐ │ PostgreSQL │ │ Valkey │ │ (users) │ │ (sessions) │ └────────────┘ └─────────────┘ optionalWithout Valkey, PostgreSQL handles both users and sessions.
Prerequisites
Section titled “Prerequisites”- A Linux server with oidc-exchange binary (see build instructions).
- PostgreSQL 14+ accessible from the server.
- (Optional) Valkey or Redis 7+ for session storage.
Step-by-step
Section titled “Step-by-step”1. Set up PostgreSQL
Section titled “1. Set up PostgreSQL”Create a database and user:
sudo -u postgres psql <<'SQL'CREATE USER oidc_exchange WITH PASSWORD 'change-me';CREATE DATABASE oidc_exchange OWNER oidc_exchange;SQLoidc-exchange runs its own migrations on startup, creating and updating the users, sessions, retired_refresh_tokens, and single_use tables. The authoritative schema is the adapter’s MIGRATIONS block in crates/adapters/src/postgres/mod.rs, applied idempotently on every start, so you never create tables by hand. If you must pre-create the schema (for example, when the application role lacks DDL permission), copy that migration verbatim rather than an approximation. In particular the users uniqueness constraint is a partial unique index on (external_id, provider) WHERE status != 'deleted', not a full unique index on external_id alone; the wrong shape blocks re-registration of a soft-deleted identity and collides the same external_id across two providers.
2. Generate a signing key
Section titled “2. Generate a signing key”sudo mkdir -p /etc/oidc-exchangeopenssl genpkey -algorithm ed25519 -out /etc/oidc-exchange/signing-key.pemchmod 600 /etc/oidc-exchange/signing-key.pem3. Configure (PostgreSQL only)
Section titled “3. Configure (PostgreSQL only)”Create /etc/oidc-exchange/config/production.toml:
[server]host = "127.0.0.1"port = 8080issuer = "https://auth.example.com"
[key_manager]adapter = "local"
[key_manager.local]private_key_path = "/etc/oidc-exchange/signing-key.pem"algorithm = "EdDSA"kid = "prod-1"
[repository]adapter = "postgres"
[repository.postgres]url = "${DATABASE_URL}"max_connections = 10
[audit]adapter = "stdout"durability = "observe"
[telemetry]enabled = trueexporter = "stdout"
[providers.google]adapter = "oidc"issuer = "https://accounts.google.com"client_id = "${GOOGLE_CLIENT_ID}"client_secret = "${GOOGLE_CLIENT_SECRET}"scopes = ["openid", "email", "profile"]# Origins Google's discovery document may name beyond the issuer's origin:endpoint_origins = ["https://oauth2.googleapis.com", "https://www.googleapis.com"]endpoint_origins pins which origins a provider’s discovery document is allowed to name; each entry must be a bare https://host[:port], and an unpinned origin logs a warning when discovered (see Identity Providers).
With this configuration, both users and sessions are stored in PostgreSQL.
4. Configure (PostgreSQL + Valkey for sessions)
Section titled “4. Configure (PostgreSQL + Valkey for sessions)”To store sessions in Valkey or Redis, add a [session_repository] section. Users remain in PostgreSQL. Sessions expire automatically when their configured lifetime ends:
[server]host = "127.0.0.1"port = 8080issuer = "https://auth.example.com"
[key_manager]adapter = "local"
[key_manager.local]private_key_path = "/etc/oidc-exchange/signing-key.pem"algorithm = "EdDSA"kid = "prod-1"
# Users in PostgreSQL[repository]adapter = "postgres"
[repository.postgres]url = "${DATABASE_URL}"max_connections = 10
# Sessions in Valkey[session_repository]adapter = "valkey"
[session_repository.valkey]url = "${VALKEY_URL}"key_prefix = "oidc:"
[audit]adapter = "stdout"durability = "observe"
[telemetry]enabled = trueexporter = "stdout"
[providers.google]adapter = "oidc"issuer = "https://accounts.google.com"client_id = "${GOOGLE_CLIENT_ID}"client_secret = "${GOOGLE_CLIENT_SECRET}"scopes = ["openid", "email", "profile"]When [session_repository] is omitted, sessions use the same adapter as [repository]. When present, it overrides only session storage.
5. Create the environment file
Section titled “5. Create the environment file”cat > /etc/oidc-exchange/env <<'EOF'DATABASE_URL=postgres://oidc_exchange:change-me@localhost:5432/oidc_exchangeGOOGLE_CLIENT_ID=your-client-idGOOGLE_CLIENT_SECRET=your-client-secretEOF
# Include Valkey URL if using the split configurationecho 'VALKEY_URL=redis://localhost:6379' >> /etc/oidc-exchange/env
chmod 600 /etc/oidc-exchange/env6. Create the systemd service
Section titled “6. Create the systemd service”[Unit]Description=oidc-exchange token serviceAfter=network-online.target postgresql.serviceWants=network-online.target
[Service]Type=simpleUser=oidc-exchangeGroup=oidc-exchangeExecStart=/usr/local/bin/oidc-exchangeWorkingDirectory=/etc/oidc-exchangeRestart=on-failureRestartSec=5
EnvironmentFile=/etc/oidc-exchange/envEnvironment=OIDC_EXCHANGE_ENV=production
NoNewPrivileges=trueProtectSystem=strictProtectHome=trueReadOnlyPaths=/etc/oidc-exchangePrivateTmp=true
[Install]WantedBy=multi-user.target7. Install and start
Section titled “7. Install and start”sudo cp target/release/oidc-exchange /usr/local/bin/sudo useradd --system --no-create-home oidc-exchangesudo systemctl daemon-reloadsudo systemctl enable --now oidc-exchange8. Reverse proxy
Section titled “8. Reverse proxy”See the generic Linux server guide for the nginx configuration. The reverse proxy setup is identical regardless of storage backend.
Connection pool tuning
Section titled “Connection pool tuning”The [repository.postgres] setting max_connections limits the database connections shared across requests. It defaults to 5. Use these starting points:
- Single instance: 10-20 connections is typical.
- Multiple instances: divide your PostgreSQL
max_connections(minus overhead) across instances. - Valkey sessions: when using Valkey for sessions, PostgreSQL handles only user CRUD, so fewer connections are needed (5-10).
Backup considerations
Section titled “Backup considerations”- PostgreSQL: standard
pg_dump/ WAL archiving covers all user data and (if not using Valkey) session data. - Valkey sessions: sessions are ephemeral by design (30-day default TTL). Valkey persistence (RDB/AOF) is optional; losing session data forces users to re-authenticate but does not lose accounts.