Skip to content

Schema Contract Locking

When Faucet auto-generates REST APIs from your database schema, any schema change — a renamed column, a dropped field, a type change — silently becomes a breaking API change. Downstream consumers discover the breakage in production.

Schema Contract Locking solves this by snapshotting the database schema as the API contract. The API shape stays stable even when the underlying database drifts. You decide when to promote changes to the API.

The Problem

Consider this flow:

  1. Backend team adds a users table with columns id, name, email
  2. Faucet generates GET /api/v1/mydb/users returning those 3 fields
  3. Frontend team builds against this API, relying on the email field
  4. DBA renames emailemail_address
  5. The API now returns email_address instead of emailfrontend breaks silently

With contract locking, step 5 never happens. The API continues serving the locked schema shape while Faucet detects the drift and alerts you.

Lock Modes

Each service has a schema_lock mode that controls how schema changes are handled:

ModeBehavior
none (default)Live schema = live API. No protection. Changes pass through instantly.
autoAdditive changes (new columns) auto-promote. Breaking changes (drops, renames, type changes) are blocked until explicitly promoted.
strictAll schema changes are blocked. Every change requires explicit promotion via faucet db promote.

Auto mode is the best default for most teams. It gives you the instant-API experience for additive changes while protecting against breaking changes.

Quick Start

Lock a Service (CLI)

bash
# Set auto mode — locks all existing tables automatically
faucet db lock mydb

# Or set strict mode
faucet db lock mydb --mode strict

Lock a Service (Admin UI)

  1. Go to Schema Explorer in the admin panel
  2. Select your service from the dropdown
  3. Change Schema Lock from "None" to "Auto" or "Strict"
  4. All tables are locked automatically

Lock a Service (API)

bash
# Set the lock mode
curl -X PUT http://localhost:8080/api/v1/system/contract/mydb/mode \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"mode": "auto"}'

# Or lock a specific table
curl -X POST http://localhost:8080/api/v1/system/contract/mydb/users \
  -H "Authorization: Bearer $TOKEN"

How It Works

Contract Snapshots

When you lock a table, Faucet snapshots the current schema — column names, types, nullability, primary keys, foreign keys — and stores it as the contract. This snapshot becomes the source of truth for the API shape.

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  Database    │────▶│  Contract    │────▶│  REST API   │
│  (live)      │     │  (snapshot)  │     │  (stable)   │
└─────────────┘     └──────────────┘     └─────────────┘
        │                   │
        │    drift check    │
        └───────────────────┘

Drift Detection

Faucet continuously compares the live database schema against the locked contract. Changes are classified into two categories:

Additive changes (safe):

  • New column added
  • Column changed from NOT NULL → nullable

Breaking changes (dangerous):

  • Column removed
  • Column type changed
  • Column changed from nullable → NOT NULL
  • Column renamed (detected as remove + add)
  • Table dropped

Auto Mode Behavior

In auto mode:

  • Additive changes are automatically promoted — the contract updates to include new columns
  • Breaking changes are blocked — DDL operations that would cause breaking changes are rejected with HTTP 409
  • The API always reflects the latest additive changes while protecting against breaking ones

Strict Mode Behavior

In strict mode:

  • All DDL operations are blocked on locked tables
  • The API shape is completely frozen
  • Every change requires explicit promotion

OpenAPI Spec Stability

When schema locking is enabled, the OpenAPI spec uses the locked contract schema instead of the live database schema. This means:

  • Downloaded OpenAPI specs are stable across database changes
  • Code generation tools produce consistent client code
  • CI/CD pipelines can diff OpenAPI specs to detect API changes

Drift Detection

Check Drift (CLI)

bash
# Check all tables in a service
faucet db diff mydb

# Example output:
# Service: mydb
# Table: users
#   ⚠ ADDITIVE  column_added: Column "bio" was added to table "users"
# Table: posts
#   ✗ BREAKING  column_removed: Column "body" was removed from table "posts"
#   ⚠ ADDITIVE  column_added: Column "content" was added to table "posts"

# Check a specific table
faucet db diff mydb --table users

# JSON output for CI
faucet db diff mydb --json

Check Drift (Admin UI)

The Schema Explorer shows drift with color-coded indicators:

IndicatorMeaning
Green dotLocked, no drift
Yellow dotAdditive drift (new columns)
Red pulsing dotBreaking drift detected

Click a table with drift to see the detailed drift report showing each change categorized as BREAKING or ADDITIVE.

Check Drift (API)

bash
# Get drift report for all locked tables
curl http://localhost:8080/api/v1/system/contract/mydb/diff \
  -H "Authorization: Bearer $TOKEN"

# Check a specific table with live drift comparison
curl "http://localhost:8080/api/v1/system/contract/mydb/users?check=true" \
  -H "Authorization: Bearer $TOKEN"

The X-Schema-Drift response header is also set on schema endpoints:

  • X-Schema-Drift: none — no drift
  • X-Schema-Drift: additive — safe changes detected
  • X-Schema-Drift: breaking — breaking changes detected

Promoting Changes

When you're ready to accept schema changes into the API contract, use the promote command:

Promote (CLI)

bash
# Promote a specific table
faucet db promote mydb --table users

# Promote all tables
faucet db promote mydb --all

Promote (Admin UI)

  1. In Schema Explorer, click a table with drift
  2. Click the Promote button in the table header
  3. The contract updates to match the live schema

Promote (API)

bash
curl -X POST http://localhost:8080/api/v1/system/contract/mydb/users/promote \
  -H "Authorization: Bearer $TOKEN"

Unlocking

Unlock (CLI)

bash
# Unlock a specific table
faucet db unlock mydb --table users

# Unlock all tables in a service
faucet db unlock mydb

Unlock (Admin UI)

  1. Click a locked table in Schema Explorer
  2. Click the Unlock button

Unlock (API)

bash
# Unlock a specific table
curl -X DELETE http://localhost:8080/api/v1/system/contract/mydb/users \
  -H "Authorization: Bearer $TOKEN"

# Unlock all tables in a service
curl -X DELETE http://localhost:8080/api/v1/system/contract/mydb \
  -H "Authorization: Bearer $TOKEN"

CI/CD Integration

Schema locking is designed to integrate with CI/CD pipelines:

Drift Check in CI

Add a drift check step to your pipeline to catch breaking changes before deployment:

yaml
# GitHub Actions example
- name: Check schema drift
  run: |
    DRIFT=$(curl -s http://faucet:8080/api/v1/system/contract/mydb/diff \
      -H "Authorization: Bearer ${{ secrets.FAUCET_API_KEY }}")

    if echo "$DRIFT" | jq -e '.has_breaking' | grep -q true; then
      echo "::error::Breaking schema changes detected!"
      echo "$DRIFT" | jq '.tables[] | select(.has_breaking) | .items[]'
      exit 1
    fi

OpenAPI Diff in CI

Compare OpenAPI specs between releases to detect API changes:

yaml
- name: Check OpenAPI changes
  run: |
    curl -s http://faucet:8080/openapi.json > current-spec.json
    diff previous-spec.json current-spec.json || {
      echo "::warning::OpenAPI spec has changed"
    }

API Reference

Endpoints

MethodPathDescription
PUT/api/v1/system/contract/{service}/modeSet lock mode (none/auto/strict)
POST/api/v1/system/contract/{service}Lock all tables in service
GET/api/v1/system/contract/{service}List all contracts
DELETE/api/v1/system/contract/{service}Unlock all tables
GET/api/v1/system/contract/{service}/diffDrift report for all tables
POST/api/v1/system/contract/{service}/{table}Lock a single table
GET/api/v1/system/contract/{service}/{table}Get contract (add ?check=true for drift)
DELETE/api/v1/system/contract/{service}/{table}Unlock a single table
POST/api/v1/system/contract/{service}/{table}/promotePromote contract to live schema

CLI Commands

faucet db lock <service> [--table <name>] [--mode auto|strict]
faucet db unlock <service> [--table <name>]
faucet db diff <service> [--table <name>] [--json]
faucet db promote <service> --table <name> | --all

Best Practices

  1. Start with auto mode — it provides the best balance of protection and convenience
  2. Check drift in CI — add faucet db diff --json to your CI pipeline to catch breaking changes early
  3. Promote intentionally — review drift reports before promoting, especially for breaking changes
  4. Lock early — enable locking as soon as downstream consumers start depending on your API
  5. Use OpenAPI specs — with locking enabled, the OpenAPI spec is stable and safe for code generation

Released under the MIT License.