# Schema Contract Locking

Source: https://wiki.faucetdb.ai/schema-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 `email` → `email_address`
5. The API now returns `email_address` instead of `email` — **frontend 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:

| Mode | Behavior |
|------|----------|
| **none** (default) | Live schema = live API. No protection. Changes pass through instantly. |
| **auto** | Additive changes (new columns) auto-promote. Breaking changes (drops, renames, type changes) are blocked until explicitly promoted. |
| **strict** | All schema changes are blocked. Every change requires explicit promotion via `faucet db promote`. |

### Recommended Mode

**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
# Snapshot every table in the service as its contract
faucet db lock mydb

# Or lock a single table
faucet db lock mydb --table users
```

`faucet db lock` saves the contracts but does not change the service's lock mode. Pick **auto** or **strict** on the [Schema page of the admin UI](https://wiki.faucetdb.ai/schema-locking#lock-a-service-admin-ui) or with the API:

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

### Lock a Service (Admin UI)

1. Open **Schema** in the [admin UI](https://wiki.faucetdb.ai/admin-ui#schema-lock).
2. Pick the database at the top right.
3. Switch **Schema lock** from **Off** to **Auto** or **Strict**. If no tables are locked yet, Faucet locks them all.
4. Lock or unlock single tables from the table header, or use **Lock all tables** and **Unlock all**.

When a locked table drifts, the table list shows a dot and the table lists each change as **Breaking** or **Additive**. **Promote changes** accepts the live shape as the new contract.

![A locked table with one breaking and one additive change](https://wiki.faucetdb.ai/screenshots/schema-locking.webp)

### 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:

| Indicator | Meaning |
|-----------|---------|
| Green dot | Locked, no drift |
| Yellow dot | Additive drift (new columns) |
| Red pulsing dot | Breaking 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

| Method | Path | Description |
|--------|------|-------------|
| `PUT` | `/api/v1/system/contract/{service}/mode` | Set 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}/diff` | Drift 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}/promote` | Promote contract to live schema |

### CLI Commands

```
faucet db lock <service> [--table <name>]
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
