DOCS / DEVELOPMENT / DATABASE & MIGRATIONS

Database & Migrations

Both apps/gateway and apps/dashboard own an independent Postgres schema, each managed by Drizzle ORM + Drizzle Kit, with identical tooling and commands — just pointed at different schema files and, typically, different databases.

GatewayDashboard
Schema fileapps/gateway/server/database/schema.tsapps/dashboard/server/database/schema.ts
Migrations dirapps/gateway/server/database/migrations/apps/dashboard/server/database/migrations/
What it ownsApplications, API keys, usage, rate limits, request logs, provider circuit/latency state — see Data Modelbetter-auth's user/session/account/verification tables only
Driverdrizzle-orm/bun-sqldrizzle-orm/bun-sql

Commands (identical in both apps)

bash
Authorization: Bearer lattice_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Run these from inside the app directory (apps/gateway or apps/dashboard), or via bun --filter gateway db:migrate / bun --filter dashboard db:migrate from the repo root.

Configuration

drizzle.config.ts in each app reads its own validated env.db object (from @xcvzlabs/env) rather than duplicating connection-string parsing:

ts
type ProviderAdapter = {
  id: ProviderId;
  createChatCompletion(
    request: ChatCompletionRequest,
    ctx: ProviderRequestContext,
  ): Promise<ChatCompletionResponse>;
  streamChatCompletion(
    request: ChatCompletionRequest,
    ctx: ProviderRequestContext,
  ): AsyncGenerator<ChatCompletionChunk, void, void>;
};

type ProviderRequestContext = {
  apiKey?: string;
  providerModel: string;
  signal: AbortSignal;
};

Because that env import goes through defineEnv, running any db:* command with a missing or invalid DB_* variable fails immediately with a clear validation error, not a cryptic connection failure.

Making a schema change

Edit the schema

Change server/database/schema.ts — add/remove a column, table, index, or enum value.

Generate a migration

bash
{
  "id": "chatcmpl-...",
  "object": "chat.completion",
  "created": 1755878400,
  "model": "gpt-4o",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "..." },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 42,
    "completion_tokens": 118,
    "total_tokens": 160
  }
}

Writes a new timestamped folder under server/database/migrations/ containing a migration.sql and a snapshot.json. Review the generated SQL — Drizzle infers a correct migration, not necessarily the safest one for a table with existing rows (e.g. adding a NOT NULL column without a default).

Apply it locally

bash
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1755878400,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1755878400,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":"Sure"},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1755878400,"model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":42,"completion_tokens":118,"total_tokens":160}}

data: [DONE]

Commit the migration folder

Migration folders are checked into version control and applied in order — they're the durable record of schema history, not just a local dev convenience.

Repositories, not raw queries in handlers

Route handlers never issue Drizzle queries directly — every table has a corresponding file under server/database/repositories/ (applications.ts, api-keys.ts, usage.ts, rate-limits.ts, request-logs.ts, circuit-state.ts, latency-state.ts, management-api-keys.ts on the gateway side) exposing narrow, purpose-built functions — e.g. findActiveApiKeyByHash, reserveUsageTokens, incrementAndCheckRateLimit. This keeps quota/rate-limit races (see Rate Limits & Quotas) contained to a single atomic query inside the repository, rather than reimplemented ad hoc wherever a handler needs one.

LATTICE — INTERNAL AI INFRASTRUCTURE
GITHUB © 2026