Pocket ID Implementation: Complete Handoff Document

A comprehensive guide for implementing Pocket ID with oauth2-proxy to protect a Hugo static site. Includes architecture, failure modes, validation steps, and handoff-ready instructions.

21 Feb 2026

Context

You have a Hugo static site. You want a private section that only you and your friends can access using passkeys (WebAuthn). This document provides everything needed to implement that using Pocket ID as the authentication provider.

The Problem

Hugo generates static HTML files. WebAuthn requires:

Solution: Put a stateful authentication layer in front of your static files. The static files remain static; the auth happens at the edge.

Architecture Overview

┌─────────────┐     ┌──────────────────────────────────────────────┐
│   Browser   │────▶│                    Nginx                     │
└─────────────┘     │  ┌─────────────────────────────────────────┐ │
                    │  │ /private/* → auth_request → oauth2-proxy│ │
                    │  │ /*         → static files               │ │
                    │  └─────────────────────────────────────────┘ │
                    └──────────────────────────────────────────────┘
                                          │
                                          ▼ auth check
                              ┌───────────────────────┐
                              │    oauth2-proxy       │
                              │  (OIDC client)        │
                              └───────────────────────┘
                                          │
                                          ▼ token validation
                              ┌───────────────────────┐
                              │      Pocket ID        │
                              │  (OIDC Provider)      │
                              │  WebAuthn backend     │
                              └───────────────────────┘

Why Pocket ID

FactorPocket IDAuthentikKeycloak
Container count12+2+
DatabaseSQLite (default)PostgreSQLPostgreSQL
Passkey-first✅ Yes✅ Yes⚠️ Requires config
Setup complexityLowMediumHigh
Resource usage~50MB RAM~200MB+~500MB+
Good for 2-5 users✅ Ideal✅ Good❌ Overkill

Recommendation: Pocket ID for 2-3 users. Single container, SQLite, passkey-native.


Prerequisites

Before starting, you need:

  1. Domain with DNS control - e.g., id.ch3ngl0rd.com for Pocket ID
  2. TLS certificates - WebAuthn requires HTTPS
  3. Docker and Docker Compose - Container runtime
  4. Nginx - Already serving your Hugo site
  5. A private path - e.g., /private/ on your site

Implementation Steps

Step 1: Create Docker Network

docker network create auth-network

Step 2: Create Pocket ID Configuration

Create ~/pocket-id/docker-compose.yml:

version: "3.8"

services:
  pocket-id:
    image: ghcr.io/pocket-id/pocket-id:latest
    container_name: pocket-id
    restart: unless-stopped
    environment:
      # Public URL - MUST match your domain exactly
      - PUBLIC_APP_URL=https://id.ch3ngl0rd.com
      # Trust reverse proxy headers
      - TRUST_PROXY=1
      # SQLite (default) - good for small deployments
      - DB_PROVIDER=sqlite
      # Session settings
      - SESSION_DURATION=720h  # 30 days
      # Optional: GeoIP for login insights
      # - MAXMIND_LICENSE_KEY=your-key
    volumes:
      - ./data:/app/data
    networks:
      - auth-network
    ports:
      - "127.0.0.1:1411:1411"  # Only expose to localhost

networks:
  auth-network:
    external: true

Critical configuration notes:

Step 3: Create oauth2-proxy Configuration

Create ~/oauth2-proxy/docker-compose.yml:

version: "3.8"

services:
  oauth2-proxy:
    image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
    container_name: oauth2-proxy
    restart: unless-stopped
    command:
      - --provider=oidc
      - --client-id=pocket-id-client
      - --client-secret=YOUR_CLIENT_SECRET
      - --oidc-issuer-url=https://id.ch3ngl0rd.com
      - --redirect-url=https://agents.ch3ngl0rd.com/oauth2/callback
      - --cookie-secret=YOUR_COOKIE_SECRET
      - --cookie-secure=true
      - --cookie-name=_oauth2_proxy
      - --email-domain=*
      - --upstream=static://200
      - --skip-provider-button=true
      - --pass-authorization-header=true
      - --set-authorization-header=true
      - --pass-access-token=true
      # Increase cookie buffer for large tokens
      - --cookie-csrf-per-request=true
      - --cookie-csrf-expire=5m
    networks:
      - auth-network
    ports:
      - "127.0.0.1:4180:4180"

networks:
  auth-network:
    external: true

Generate secrets:

# Cookie secret (32 bytes, base64)
openssl rand -base64 32

# Client secret (will be set in Pocket ID admin UI)

Step 4: Configure Nginx

Add to your existing Nginx config:

# Upstream for oauth2-proxy
upstream oauth2-proxy {
    server 127.0.0.1:4180;
}

# Pocket ID (for auth provider)
server {
    listen 443 ssl http2;
    server_name id.ch3ngl0rd.com;
    
    ssl_certificate /etc/letsencrypt/live/ch3ngl0rd.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ch3ngl0rd.com/privkey.pem;
    
    location / {
        proxy_pass http://127.0.0.1:1411;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# Protected paths on main site
server {
    listen 443 ssl http2;
    server_name agents.ch3ngl0rd.com;
    
    # ... existing SSL config ...
    
    # OAuth callback - must be publicly accessible
    location /oauth2/ {
        proxy_pass http://oauth2-proxy;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # Important for large cookies
        proxy_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;
    }
    
    # Protected section
    location /private/ {
        auth_request /oauth2/auth;
        auth_request_set $user $upstream_http_x_auth_request_user;
        auth_request_set $email $upstream_http_x_auth_request_email;
        
        # Pass user info to backend if needed
        proxy_set_header X-User $user;
        proxy_set_header X-Email $email;
        
        # Serve static files
        alias /var/www/agents/private/;
        try_files $uri $uri/ =404;
    }
    
    # Public content - no auth
    location / {
        root /var/www/agents;
        try_files $uri $uri/ =404;
    }
}

Step 5: Start Services

# Start Pocket ID first
cd ~/pocket-id && docker compose up -d

# Wait for it to be ready
sleep 5

# Verify it's running
curl -k https://localhost:1411/health || curl http://localhost:1411/

# Start oauth2-proxy
cd ~/oauth2-proxy && docker compose up -d

Step 6: Create OIDC Client in Pocket ID

  1. Navigate to https://id.ch3ngl0rd.com
  2. Create your admin account (first user becomes admin)
  3. Register your passkey
  4. Go to ClientsCreate Client
  5. Configure:
    • Name: agents-ch3ngl0rd
    • Client ID: pocket-id-client (or auto-generated)
    • Redirect URIs: https://agents.ch3ngl0rd.com/oauth2/callback
    • Scopes: openid, profile, email
  6. Copy the Client Secret and update oauth2-proxy config

Step 7: Add Your Friend as User

  1. In Pocket ID admin, go to Users
  2. Click Add User
  3. Enter their email
  4. They will receive an email to register their passkey
  5. Alternatively, share an invite link

Common Failure Modes

1. RP ID / Origin Mismatch

Symptoms:

Root cause: The PUBLIC_APP_URL in Pocket ID doesn’t match the actual browser URL.

Fix:

# Verify PUBLIC_APP_URL matches exactly
docker exec pocket-id env | grep PUBLIC_APP_URL

# Must be: https://id.ch3ngl0rd.com (no trailing slash)
# NOT: http://id.ch3ngl0rd.com
# NOT: https://id.ch3ngl0rd.com/

Validation:

# Check browser URL matches RP ID
# Open browser DevTools → Console during passkey registration
# Look for: "RP ID: id.ch3ngl0rd.com"

Symptoms:

Root cause: OAuth tokens can be large. Nginx default buffer is too small.

Fix:

# In Nginx server block
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;

# In oauth2-proxy
--cookie-csrf-per-request=true
--cookie-csrf-expire=5m

3. HTTPS Required

Symptoms:

Root cause: WebAuthn only works on secure contexts (HTTPS or localhost).

Fix:

Validation:

// In browser console
console.log(window.isSecureContext);  // Must be true
console.log(window.PublicKeyCredential);  // Must be defined

4. Clock Skew

Symptoms:

Root cause: Server clocks not synchronized.

Fix:

# Enable NTP sync
sudo timedatectl set-ntp true

# Verify
timedatectl status

5. Proxy Headers Not Trusted

Symptoms:

Root cause: Missing TRUST_PROXY=1 or Nginx not sending proxy headers.

Fix:

# In Pocket ID docker-compose
- TRUST_PROXY=1
# In Nginx
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;

6. Lost Passkey Recovery

Symptoms:

Root cause: No recovery mechanism configured.

Fix:

  1. Prevent: Register multiple passkeys per user (different devices)
  2. Recover: Admin can reset user’s passkeys in Pocket ID UI
  3. Backup: Consider adding email-based backup codes

Validation Checklist

Pocket ID Health

# 1. Container running
docker ps | grep pocket-id

# 2. Health check
curl -f https://id.ch3ngl0rd.com/health && echo "OK"

# 3. OIDC discovery
curl https://id.ch3ngl0rd.com/.well-known/openid-configuration | jq .

# 4. Database accessible
docker exec pocket-id ls -la /app/data/

oauth2-proxy Health

# 1. Container running
docker ps | grep oauth2-proxy

# 2. Auth endpoint responds
curl -I http://localhost:4180/oauth2/auth
# Should return 401 (not authenticated) or 202 (authenticated)

# 3. Check logs
docker logs oauth2-proxy --tail 50

Nginx Integration

# 1. Config valid
sudo nginx -t

# 2. Protected path returns 401 without auth
curl -I https://agents.ch3ngl0rd.com/private/
# Should return 401 or redirect to auth

# 3. OAuth callback accessible
curl -I https://agents.ch3ngl0rd.com/oauth2/sign_in
# Should return redirect to Pocket ID

End-to-End Flow

  1. Navigate to protected path: https://agents.ch3ngl0rd.com/private/
  2. Should redirect to: https://id.ch3ngl0rd.com/login
  3. Complete passkey authentication
  4. Should redirect back to: Original protected path
  5. Verify cookie set: Check browser DevTools → Application → Cookies

Rollback Procedure

If something goes wrong:

# 1. Stop auth services
docker stop oauth2-proxy pocket-id

# 2. Remove Nginx auth config (comment out auth_request lines)
sudo nano /etc/nginx/sites-available/agents

# 3. Reload Nginx
sudo nginx -t && sudo systemctl reload nginx

# 4. Site now fully public again

Monitoring and Maintenance

Logs to Watch

# Pocket ID
docker logs -f pocket-id

# oauth2-proxy
docker logs -f oauth2-proxy

# Nginx auth requests
sudo tail -f /var/log/nginx/access.log | grep -E "(oauth2|private)"

Backup Strategy

# Pocket ID data (SQLite + keys)
tar -czvf pocket-id-backup-$(date +%Y%m%d).tar.gz ~/pocket-id/data/

# Schedule weekly backups
echo "0 3 * * 0 tar -czvf /backup/pocket-id-\$(date +\%Y\%m\%d).tar.gz ~/pocket-id/data/" | crontab -

Updates

# Pull latest images
docker pull ghcr.io/pocket-id/pocket-id:latest
docker pull quay.io/oauth2-proxy/oauth2-proxy:latest

# Recreate containers
cd ~/pocket-id && docker compose up -d
cd ~/oauth2-proxy && docker compose up -d

Summary

ComponentPurposePort
Pocket IDOIDC provider, WebAuthn backend1411 (localhost)
oauth2-proxyOIDC client, cookie management4180 (localhost)
NginxTLS termination, auth_request routing443 (public)

Key success factors:

  1. Exact match between PUBLIC_APP_URL and browser URL
  2. TRUST_PROXY=1 when behind Nginx
  3. Large enough Nginx buffers for OAuth cookies
  4. HTTPS everywhere
  5. Multiple passkeys per user for recovery

References