Drift Scanner

API reference

HTTP API for Drift Scanner. Register and list environments, trigger scans, diff environments, analyze migrations, and paginate through drift events.

The Drift Scanner API lives at https://drift.arcnull.com. All responses are wrapped in a standard envelope:

{
  "success": true,
  "data": { /* typed payload */ },
  "message": "Human-readable message (optional)",
  "errorCode": null,
  "timestamp": "2026-04-20T15:10:22.187Z"
}

Base URL

https://drift.arcnull.com

Authentication

Every endpoint requires a JWT issued by the platform auth service. Pass it as a bearer token:

Authorization: Bearer <jwt>

You can mint a JWT by logging in through https://arcnull.com and pulling the token from your dashboard, or by calling the auth API directly (see platform docs — coming soon).

Customer API keys are coming soon. A long-lived API key flow for server-to-server usage is on the roadmap. For now, use a JWT from a logged-in session. When API keys ship, you will be able to mint up to 1 (free) / 5 (pro) / 10+ (growth+) per tenant.

Errors

Errors return success: false with an errorCode and message:

{
  "success": false,
  "data": null,
  "message": "Plan limit reached: your current plan allows up to 2 drift environments. Upgrade your plan to add more.",
  "errorCode": "BAD_REQUEST",
  "timestamp": "2026-04-20T15:10:22.187Z"
}

Common HTTP statuses:

  • 400 — validation / plan-limit / SSRF rejection
  • 401 — missing or invalid JWT
  • 404 — resource not found for this tenant
  • 429 — manual scan rate limit exceeded

Environments

Register an environment

POST /api/v1/drift/environments

Request:

{
  "name": "production",
  "jdbcUrl": "jdbc:postgresql://prod-db.example.com:5432/myapp",
  "username": "arcnull_scanner",
  "password": "<strong-password>",
  "scanCron": "0 0 * * * *"
}
  • name — 1-100 chars, must start with a letter or digit, letters/digits/spaces/hyphens/underscores
  • jdbcUrl — must match jdbc:postgresql://host/db(?params)?, must not resolve to loopback, private, link-local, metadata, or CGNAT ranges
  • username — valid PostgreSQL identifier (up to 63 chars)
  • password — up to 512 chars
  • scanCron — optional Spring cron expression (6 fields); defaults to 0 0 * * * * (top of every hour)

Response: 201 Created with the new environment.

curl -X POST https://drift.arcnull.com/api/v1/drift/environments \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "production",
    "jdbcUrl": "jdbc:postgresql://prod-db.example.com:5432/myapp",
    "username": "arcnull_scanner",
    "password": "secret",
    "scanCron": "0 0 * * * *"
  }'

List environments

GET /api/v1/drift/environments

Returns all active environments for the authenticated tenant.

{
  "success": true,
  "data": [
    {
      "id": "18a2d7be-4f52-4a86-92b0-52df7d39c7a1",
      "name": "production",
      "status": "READY",
      "scanCron": "0 0 * * * *",
      "active": true,
      "lastScanned": "2026-04-20T15:00:00Z",
      "createdAt": "2026-04-19T09:14:23Z"
    }
  ]
}

Get an environment

GET /api/v1/drift/environments/{id}

Deactivate an environment

DELETE /api/v1/drift/environments/{id}

Sets the environment's active flag to false. History is retained. Returns 200 OK.


Scanning

Trigger a manual scan

POST /api/v1/drift/environments/{id}/scan

Rate-limited per tier (free: 3/min, pro: 10/min). Returns the snapshot that was produced:

{
  "success": true,
  "data": {
    "id": "b7d1a3c4-2a3e-4b7e-9a4f-1c2e9b0d3a77",
    "checksum": "sha256:9c3a...",
    "tablesCount": 42,
    "columnsCount": 317,
    "indexesCount": 88,
    "capturedAt": "2026-04-20T15:10:22.187Z"
  },
  "message": "Scan completed"
}
curl -X POST https://drift.arcnull.com/api/v1/drift/environments/$ENV_ID/scan \
  -H "Authorization: Bearer $JWT"

Compare two environments

POST /api/v1/drift/compare

Diffs the latest snapshot of one environment against another — useful for "did staging catch everything production ships?"

Request:

{
  "baseEnvId": "18a2d7be-4f52-4a86-92b0-52df7d39c7a1",
  "headEnvId": "7c1d4e2a-9b3f-4a1c-8d02-3e47f9b8c211"
}

Response:

{
  "success": true,
  "data": {
    "severity": "WARNING",
    "breakingCount": 0,
    "warningCount": 1,
    "infoCount": 3,
    "items": [
      {
        "severity": "WARNING",
        "table": "orders",
        "column": "shipping_country",
        "changeType": "COLUMN_ADDED_NOT_NULL_NO_DEFAULT",
        "description": "orders.shipping_country added as NOT NULL without default",
        "recommendation": "Backfill and add a default before rolling forward.",
        "estimatedImpact": "Inserts from older code paths will fail."
      }
    ]
  }
}

Migration analysis

Run a proposed CREATE TABLE / ALTER TABLE / DROP statement through the analyzer to get a pre-flight safety report.

POST /api/v1/drift/migrations/analyze

Request:

{
  "sql": "ALTER TABLE users ADD COLUMN email_verified BOOLEAN NOT NULL;",
  "environmentId": "18a2d7be-4f52-4a86-92b0-52df7d39c7a1"
}

Response:

{
  "success": true,
  "data": {
    "safe": false,
    "overallRisk": "HIGH",
    "recommendation": "Add as NULL, backfill, then tighten to NOT NULL.",
    "operations": [
      {
        "sql": "ALTER TABLE users ADD COLUMN email_verified BOOLEAN NOT NULL;",
        "operationType": "ADD_COLUMN",
        "tableName": "users",
        "lockType": "ACCESS EXCLUSIVE",
        "estimatedDuration": "~45s",
        "tableSize": 2147483648,
        "rowCount": 1200000,
        "safe": false,
        "recommendation": "Adding a NOT NULL column without a default rewrites the table.",
        "safeAlternative": "ALTER TABLE users ADD COLUMN email_verified BOOLEAN; /* backfill */; ALTER TABLE users ALTER COLUMN email_verified SET NOT NULL;"
      }
    ]
  }
}

Drift events

List events

GET /api/v1/drift/events?page=0&size=20

Paginated. Default size is 20, max 100.

{
  "success": true,
  "data": {
    "content": [
      {
        "id": "f6c3a3f0-7c5e-4a8c-b4b2-9f3b1f0c4a55",
        "envId": "18a2d7be-4f52-4a86-92b0-52df7d39c7a1",
        "baselineId": "0ce3b9a2-...",
        "currentId": "b7d1a3c4-...",
        "severity": "BREAKING",
        "breakingCount": 1,
        "warningCount": 0,
        "infoCount": 2,
        "acknowledged": false,
        "detectedAt": "2026-04-20T15:10:22.187Z",
        "items": [ /* ... */ ]
      }
    ],
    "totalElements": 34,
    "totalPages": 2,
    "number": 0,
    "size": 20
  }
}

Get a single event

GET /api/v1/drift/events/{id}

Acknowledge an event

POST /api/v1/drift/events/{id}/acknowledge

Marks the event as reviewed. Does not affect the baseline.


Response envelope reference

FieldTypeNotes
successbooleantrue for 2xx, false otherwise
dataT | nullTyped payload, null on error
messagestring?Human-readable hint, useful for surfacing in UIs
errorCodestring?Machine-readable code on error (e.g. BAD_REQUEST)
timestampstringISO-8601 response time, useful for correlating with logs