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.
| Gateway | Dashboard | |
|---|---|---|
| Schema file | apps/gateway/server/database/schema.ts | apps/dashboard/server/database/schema.ts |
| Migrations dir | apps/gateway/server/database/migrations/ | apps/dashboard/server/database/migrations/ |
| What it owns | Applications, API keys, usage, rate limits, request logs, provider circuit/latency state — see Data Model | better-auth's user/session/account/verification tables only |
| Driver | drizzle-orm/bun-sql | drizzle-orm/bun-sql |
Commands (identical in both apps)
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:
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
{
"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
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.