Skip to content

A database for your GitHub Actions

GitHub Actions has no native way to persist data between workflow runs — the cache expires after 7 days and can’t be updated in place, and artifacts need a run ID to fetch. PerSQL gives a workflow a real, persistent database instead: it survives across runs, and authentication is keyless — the workflow proves its identity with a GitHub OIDC token, so there’s no secret to store.

  1. Install the PerSQL GitHub App on the repo. (That’s the binding from your repo to your PerSQL namespace.)
  2. Give the job id-token: write — the only permission needed.
permissions:
id-token: write
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: db
uses: persql/setup-db@v1 # connects keyless; outputs token + url
- name: Use the database
env:
TOKEN: ${{ steps.db.outputs.token }}
URL: ${{ steps.db.outputs.url }}
run: |
curl -sf -X POST "$URL/query" \
-H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"sql":"CREATE TABLE IF NOT EXISTS runs (id INTEGER PRIMARY KEY AUTOINCREMENT, at TEXT DEFAULT (datetime('"'"'now'"'"')))"}'
curl -sf -X POST "$URL/query" \
-H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"sql":"INSERT INTO runs DEFAULT VALUES"}'
# This count grows by one every run — the data persisted.
curl -sf -X POST "$URL/query" \
-H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"sql":"SELECT count(*) AS total_runs FROM runs"}'

The action is a thin wrapper over one HTTP call — you can do it by hand if you’d rather not add a dependency:

Terminal window
# Mint a GitHub OIDC token (id-token: write makes these env vars available)
OIDC=$(curl -sf -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=persql" | jq -r .value)
# Exchange it for a short-lived, repo-scoped PerSQL token
curl -sf -X POST https://api.persql.com/v1/actions/connect \
-H "Authorization: Bearer $OIDC"
# -> { "data": { "token": "...", "url": ".../v1/db/<ns>/<db>", "database": "...", "namespace": "..." } }
  • The workflow mints a short-lived GitHub OIDC token (audience: persql) that proves this run is in repo X.
  • PerSQL verifies it against GitHub’s public keys, maps the repo to its namespace, and provisions a per-repo database (gh-<owner>-<name>-actions).
  • You get back a short-lived token scoped to that one database — full control of it, no reach to anything else. Reads and writes go through the standard /v1 API.

Nothing is ever written to your repo’s secrets. The token expires on its own; the database persists.

  • It’s SQLite. Great for run state, accumulating results, cron-style data collection, coordination between runs.
  • Billing is usage-only against your namespace balance — you pay only for the requests, rows, and storage a run actually uses.
  • Concurrent runs share the same database (it’s one DB per repo). Use SQL the way you would anywhere — e.g. INSERT OR REPLACE, UNIQUE constraints — if runs might race.