Skip to main content

Ephemeral Databases for Every PR: Using the Filess Shared API in CI/CD

· 6 min read
Filess Team
Database Experts

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 APIDedicated API
Base URLhttps://api.filess.iohttps://backend.filess.io
ResourcesInstances (/v1/instances)Databases (/v1/databases)
ModelSimple: identifier + region + motorOrganizations + Namespaces + plan config
AuthBearer JWT tokenBearer API token
Best forCI/CD, prototypes, ephemeralProduction, teams, stable projects
Connection infoReturned immediately on creationRetrieved 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.

Get started with the Filess Shared API →