Ephemeral Databases for Every PR: Using the Filess Shared API in CI/CD
You've built a feature. It works on your machine. You open a PR.
Your CI pipeline runs unit tests. They pass. Your reviewer approves the PR.
You merge. You deploy. Production breaks.
The bug was a subtle interaction between your code and the database schema — something unit tests couldn't catch because they mock the database. Integration tests could have caught it, but your integration test suite either doesn't exist or runs against a shared staging database that's full of dirty state from the last developer who also tested there.
The real fix is an ephemeral database per PR: a fresh database, identical to production schema, provisioned when the PR opens and destroyed when it merges. Your integration tests run against it. Nothing shares state between PRs.
Filess has two APIs. Knowing which one to use matters.
Two APIs, Two Use Cases
Filess exposes two distinct REST APIs:
| Shared API | Dedicated API | |
|---|---|---|
| Base URL | https://api.filess.io | https://backend.filess.io |
| Resources | Instances (/v1/instances) | Databases (/v1/databases) |
| Model | Simple: identifier + region + motor | Organizations + Namespaces + plan config |
| Auth | Bearer JWT token | Bearer API token |
| Best for | CI/CD, prototypes, ephemeral | Production, teams, stable projects |
| Connection info | Returned immediately on creation | Retrieved via separate detail call |
For ephemeral CI/CD databases, the Shared API is the right choice. It's designed for this: three fields in, connection string out. No org slugs, no namespace slugs, no plan configuration.
The Dedicated API (backend.filess.io) is for long-lived databases organized within your team's Organizations and Namespaces — more control, more configuration, better suited for stable environments.
The Shared API: Three Endpoints
GET https://api.filess.io/v1/regions — List available regions
POST https://api.filess.io/v1/instances — Create a database
DELETE https://api.filess.io/v1/instances/{id} — Delete a database
That's the entire surface for CI/CD. Clean.
Create an instance
curl -X POST "https://api.filess.io/v1/instances" \
-H "Authorization: Bearer <your-jwt-token>" \
-H "Content-Type: application/json" \
-d '{
"identifier": "pr-452-test",
"region": "4747ed15-34b7-4f41-a80b-387a6a907a1e",
"motor": "mysql-8.0.29"
}'
Response — connection details returned immediately, no second request needed:
{
"msg": "OK",
"data": {
"instance": {
"id": "14f831d2-923c-4352-86d0-10f06c6272ae",
"name": "pr452test_acceptinch",
"user": "pr452test_acceptinch",
"isEnabled": true,
"identifier": "pr-452-test",
"host": "z6a.h.filess.io",
"port": 3306,
"region": "Spain",
"password": "dd976df83454d5a9bb259995ae82f222ac82e59d",
"createdAt": "2026-04-04T10:00:00.000Z"
}
}
}
Available motors
mysql-5.7.38
mysql-8.0.29
mariadb-10.7.4
postgresql-14.4.0
postgresql-15.4.0
mongodb-5.0.9
mongodb-7.0.2
Delete an instance
curl -X DELETE "https://api.filess.io/v1/instances/14f831d2-923c-4352-86d0-10f06c6272ae" \
-H "Authorization: Bearer <your-jwt-token>"
Complete GitHub Actions Workflow
# .github/workflows/integration-tests.yml
name: Integration Tests
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
test:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get region ID
id: region
env:
FILESS_TOKEN: ${{ secrets.FILESS_SHARED_TOKEN }}
run: |
REGION_ID=$(curl -sf "https://api.filess.io/v1/regions" \
-H "Authorization: Bearer $FILESS_TOKEN" \
| jq -r '.data.regions[0].id')
echo "id=$REGION_ID" >> $GITHUB_OUTPUT
- name: Create ephemeral database
id: db
env:
FILESS_TOKEN: ${{ secrets.FILESS_SHARED_TOKEN }}
run: |
RESPONSE=$(curl -sf -X POST "https://api.filess.io/v1/instances" \
-H "Authorization: Bearer $FILESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"identifier\": \"pr-${{ github.event.number }}\",
\"region\": \"${{ steps.region.outputs.id }}\",
\"motor\": \"mysql-8.0.29\"
}")
DB_ID=$(echo $RESPONSE | jq -r '.data.instance.id')
DB_HOST=$(echo $RESPONSE | jq -r '.data.instance.host')
DB_PORT=$(echo $RESPONSE | jq -r '.data.instance.port')
DB_USER=$(echo $RESPONSE | jq -r '.data.instance.user')
DB_PASS=$(echo $RESPONSE | jq -r '.data.instance.password')
DB_NAME=$(echo $RESPONSE | jq -r '.data.instance.name')
# Build connection URL
DB_URL="mysql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
echo "db_id=$DB_ID" >> $GITHUB_OUTPUT
echo "db_url=$DB_URL" >> $GITHUB_OUTPUT
- name: Store DB ID for cleanup
uses: actions/cache/save@v4
with:
path: /dev/null # dummy, we use the key
key: filess-db-pr-${{ github.event.number }}-${{ steps.db.outputs.db_id }}
- name: Run migrations
env:
DATABASE_URL: ${{ steps.db.outputs.db_url }}
run: npx prisma migrate deploy
- name: Run integration tests
env:
DATABASE_URL: ${{ steps.db.outputs.db_url }}
run: npm test -- --testPathPattern=integration
- name: Delete database on test failure
if: failure()
env:
FILESS_TOKEN: ${{ secrets.FILESS_SHARED_TOKEN }}
run: |
curl -sf -X DELETE \
"https://api.filess.io/v1/instances/${{ steps.db.outputs.db_id }}" \
-H "Authorization: Bearer $FILESS_TOKEN"
teardown:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Restore cached DB ID
id: cache
uses: actions/cache/restore@v4
with:
path: /dev/null
key: filess-db-pr-${{ github.event.number }}-
restore-keys: filess-db-pr-${{ github.event.number }}-
- name: Delete database
env:
FILESS_TOKEN: ${{ secrets.FILESS_SHARED_TOKEN }}
run: |
# Extract DB ID from the matched cache key
DB_ID=$(echo "${{ steps.cache.outputs.cache-matched-key }}" \
| sed 's/filess-db-pr-[0-9]*-//')
curl -sf -X DELETE \
"https://api.filess.io/v1/instances/$DB_ID" \
-H "Authorization: Bearer $FILESS_TOKEN"
echo "Deleted database $DB_ID"
A Minimal Shell Helper
For projects that use multiple CI systems or custom scripts:
#!/usr/bin/env bash
# scripts/filess.sh — Shared API wrapper
set -e
TOKEN="${FILESS_SHARED_TOKEN:?FILESS_SHARED_TOKEN must be set}"
BASE="https://api.filess.io"
filess_regions() {
curl -sf "$BASE/v1/regions" \
-H "Authorization: Bearer $TOKEN" \
| jq -r '.data.regions[] | "\(.id)\t\(.name)"'
}
filess_create() {
local name="$1" region="$2" motor="${3:-mysql-8.0.29}"
curl -sf -X POST "$BASE/v1/instances" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"identifier\":\"$name\",\"region\":\"$region\",\"motor\":\"$motor\"}"
}
filess_delete() {
local id="$1"
curl -sf -X DELETE "$BASE/v1/instances/$id" \
-H "Authorization: Bearer $TOKEN"
}
case "$1" in
regions) filess_regions ;;
create) filess_create "$2" "$3" "$4" ;;
delete) filess_delete "$2" ;;
*) echo "Usage: $0 regions|create <name> <region> [motor]|delete <id>"; exit 1 ;;
esac
Usage:
# Pick a region
./scripts/filess.sh regions
# → 4747ed15-34b7-4f41-a80b-387a6a907a1e Spain
# Create
RESULT=$(./scripts/filess.sh create "pr-452" "4747ed15-34b7-4f41-a80b-387a6a907a1e")
DB_ID=$(echo $RESULT | jq -r '.data.instance.id')
DB_HOST=$(echo $RESULT | jq -r '.data.instance.host')
DB_USER=$(echo $RESULT | jq -r '.data.instance.user')
DB_PASS=$(echo $RESULT | jq -r '.data.instance.password')
DB_NAME=$(echo $RESULT | jq -r '.data.instance.name')
# Delete
./scripts/filess.sh delete $DB_ID
When to Use Each API
Shared API (api.filess.io) — for:
- CI/CD pipelines: One database per PR, destroyed on merge.
- Prototypes: Spin up a database in 30 seconds to test an idea.
- Classroom / hackathon: Give each participant their own fresh database without managing accounts.
- Automated test suites: Integration tests that need a real database engine, not a mock.
Dedicated API (backend.filess.io) — for:
- Production databases: Long-lived, monitored, backed up.
- Teams with RBAC: Organized under Organizations and Namespaces with granular permissions.
- Infrastructure-as-code: Provision databases as part of a Terraform or API-driven infrastructure workflow.
- Databases that need addons: Tailscale, firewall rules, PITR, SSH tunnels, PMM monitoring.
The Shared API is fast and frictionless by design. The Dedicated API gives you full control. Use the right one for the job.
Why Ephemeral Databases Beat Shared Staging
The shared staging database pattern has real costs:
- Test flakiness: Tests pass or fail based on what a previous test run left behind, not on your code.
- Parallelism breaks: Two PRs can't simultaneously test a migration that adds and removes the same column.
- Cleanup debt: You need teardown scripts that are harder to maintain than the tests themselves.
- Data leakage: PII from database dumps living in a shared environment everyone on the team can access.
Ephemeral databases eliminate all of this. Each PR gets a clean slate. Migrations either work or they fail clearly. Parallel PR builds never interfere. The database is destroyed on merge, taking all test data with it.
The cost: a few seconds to provision and a few cents per PR on your Filess bill.
