Backup, upgrade, purge

TL;DR — Three operational chores. Backup the storage driver (SQLite file / pg_dump) plus the signing keys directory. Upgrade by pulling the new binary or image and running authserver migrate; migrations are forward-only and idempotent. Purge is not automatic in serve — schedule authserver purge externally (systemd timer, Docker sidecar, or k8s CronJob) or expired-data tables grow unbounded.

Backup

SQLite

The database file and signing keys must be backed up together.

Cold backup (safe, requires stopping the server):

# Standalone
systemctl stop authserver
cp /var/lib/authserver/authserver.db /backup/authserver-$(date +%Y%m%d).db
cp -r /var/lib/authserver/keys /backup/keys-$(date +%Y%m%d)
systemctl start authserver

# Docker Compose
docker compose stop authserver
docker run --rm -v authserver-data:/data -v $(pwd)/backup:/backup \
    alpine tar czf /backup/authserver-$(date +%Y%m%d).tar.gz -C /data .
docker compose start authserver

Live backup (no downtime, WAL mode required):

sqlite3 /var/lib/authserver/authserver.db \
    ".backup /backup/authserver-$(date +%Y%m%d).db"

SQLite’s .backup command holds a shared lock briefly and produces a consistent snapshot while writes continue. Enable WAL mode (default) for concurrent-read semantics: storage.sqlite.wal: true.

PostgreSQL

# One-shot
docker compose exec postgres pg_dump -U authserver authserver > backup.sql

# Or from a standalone Postgres host
pg_dump -h db.internal -U authserver authserver > backup.sql

For production, use continuous archiving (WAL-E, pgBackRest, barman) for point-in-time recovery. AuthPlane’s schema is standard PostgreSQL — any Postgres-aware backup tool works.

Signing keys

Always back up the signing keys directory. If you lose them, every outstanding JWT becomes unverifiable — clients will hit 401 invalid_token until they re-authenticate.

  • Keyfile store — back up /var/lib/authserver/keys/ (standalone) or the authserver-data volume (Docker) or the persistent volume (Kubernetes).
  • postgres_key store — keys are in the Postgres backup already; nothing extra to back up.
  • vault_transit store — Vault is the source of truth. Follow your Vault backup procedure; AuthPlane never has the private key on disk.

Upgrade

AuthPlane follows semantic versioning. Same-major-line upgrades are drop-in; a major bump signals a breaking change and comes with a migration note in the release.

Standalone binary:

systemctl stop authserver
cp /usr/local/bin/authserver /usr/local/bin/authserver.bak
curl -L https://github.com/authplane/authserver/releases/latest/download/authserver-linux-amd64 \
    -o /usr/local/bin/authserver
chmod +x /usr/local/bin/authserver
sudo -u authserver authserver migrate --config /etc/authserver/config.yaml
systemctl start authserver

Docker Compose:

docker compose pull authserver
docker compose run --rm authserver migrate     # apply any new migrations
docker compose up -d

Kubernetes (Helm):

helm upgrade authplane oci://ghcr.io/authplane/charts/authplane \
    --version <new-version> \
    -f values-production.yaml

The Helm chart runs migrations automatically on pod boot — no separate migration Job needed. The init container waits for Postgres connectivity; the main process embeds all migration SQL via go:embed and applies pending migrations before serving traffic.

Migrations are forward-only and idempotent

Running authserver migrate on an already-migrated database is a no-op. There is no rollback — restore from backup if you need to revert.

Read the release notes before major-version bumps. Breaking changes are called out in CHANGELOG.md with concrete migration steps.

Scheduled purge

serve does not run purge goroutines. With dpop.enabled, client_credentials.enabled, or xaa.enabled set to true, you MUST schedule authserver purge externally or the expired-data tables grow unbounded.

What authserver purge deletes

One pass deletes expired rows from every purgeable table:

Target (--only name)What it deletes
assertion-jtiExpired XAA / JWT-bearer assertion JTIs (replay prevention)
connect-pending-statesExpired upstream-connect pending states (broker Connect flow)
dpop-noncesExpired DPoP proof JTIs and server nonces
jtiExpired revoked token JTIs (RFC 7009 revocation list)
machine-tokensExpired client-credentials machine tokens
refresh-tokensExpired refresh tokens and aged-out token families
sessionsExpired user sessions

Default is all targets; --only=target1,target2 runs a subset. --timeout defaults to 10m; pass --timeout=0 to disable the internal deadline. Aborts on SIGINT/SIGTERM.

Consent grants and other never-expire rows are not touched — they age out via revocation, not expiration.

A single daily run is enough for most deployments. Operators who issue many short-lived tokens (DPoP proofs, machine tokens) may prefer hourly to keep tables small. The purge is a set of lightweight DELETE WHERE expires_at < now() queries — it does not block OAuth traffic.

Scheduled purge — systemd timer

For standalone binary deployments.

/etc/systemd/system/authserver-purge.service:

[Unit]
Description=AuthPlane expired-data purge
After=network-online.target

[Service]
Type=oneshot
User=authserver
Group=authserver
ExecStart=/usr/local/bin/authserver purge --config /etc/authserver/config.yaml

/etc/systemd/system/authserver-purge.timer:

[Unit]
Description=Run authserver purge hourly

[Timer]
OnCalendar=hourly
RandomizedDelaySec=5m
Persistent=true

[Install]
WantedBy=timers.target

Enable:

systemctl daemon-reload
systemctl enable --now authserver-purge.timer
systemctl list-timers authserver-purge.timer

Inspect recent runs:

journalctl -u authserver-purge.service --since '1 day ago'

Scheduled purge — Docker Compose sidecar

Add a second service alongside authserver that reuses the same image and env — kept out of docker compose up by default via profiles: ["purge"]:

services:
  authserver:
    image: authplane/authserver:latest
    # ... your normal config ...

  authserver-purge:
    image: authplane/authserver:latest
    command: ["purge"]
    environment:
      # Must match authserver's storage config — purge hits the same DB.
      AUTHPLANE_STORAGE_DRIVER: postgres
      AUTHPLANE_STORAGE_POSTGRES_DSN: postgres://authserver:${POSTGRES_PASSWORD}@postgres:5432/authserver?sslmode=disable
    depends_on:
      postgres:
        condition: service_healthy
    restart: "no"
    profiles: ["purge"]

Trigger from the host — cron or on demand:

# On-demand
docker compose run --rm authserver-purge

# Hourly via host crontab (crontab -e)
0 * * * * cd /opt/authserver && docker compose run --rm authserver-purge >> /var/log/authserver-purge.log 2>&1

Prefer host-level scheduling over stuffing cron inside the container — keeps the image minimal and failures visible to your standard monitoring.

Scheduled purge — Kubernetes CronJob

apiVersion: batch/v1
kind: CronJob
metadata:
  name: authserver-purge
  namespace: authserver
spec:
  schedule: "0 * * * *"             # hourly
  concurrencyPolicy: Forbid          # don't overlap runs
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 1
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: purge
              image: authplane/authserver:latest
              args: ["purge"]
              envFrom:
                - configMapRef:
                    name: authserver-config
                - secretRef:
                    name: authserver-secrets
              resources:
                requests: { cpu: 50m,  memory: 64Mi }
                limits:   { cpu: 500m, memory: 256Mi }

The CronJob must share the same storage config (AUTHPLANE_STORAGE_* env) as the serve Deployment — it runs purge against the same database.

For “rows purged over time” charts, enable metrics on the CronJob pod the same way as serve (e.g. AUTHPLANE_METRICS_PROVIDER=otel with the OTLP endpoint).

Selective purge

Run just the high-churn tables hourly and everything else daily:

# Hourly
authserver purge --only=dpop-nonces,assertion-jti,jti --config /etc/authserver/config.yaml

# Daily
authserver purge --config /etc/authserver/config.yaml

--only accepts comma-separated target names from the table above.

Exit codes and alerting

authserver purge exits non-zero if any target fails or the context is canceled. Individual failures log at ERROR with a table=<target> attribute; the command continues with remaining targets and fails at the end. Wire the job’s exit status into your alerting:

  • systemdOnFailure= in the service unit
  • Kubernetes — the CronJob’s Job failure events; alert on kube_job_status_failed > 0
  • Docker cron wrapper — grep the log for ERROR and page on non-zero exit

Verifying a scheduled purge

Run manually once after setup:

authserver purge --timeout=5m

You should see INFO purge completed for each target.

What’s NOT part of these chores

  • Rate-limiter cache — cleaned in-process every 5 minutes; not persisted, no scheduling needed.
  • JWKS cache — invalidated by SIGHUP or POST /admin/keys/rotate; TTL-less otherwise.
  • In-memory registry caches — none. The unified resource model reads from the DB on every request.