Skip to content
AdStack Engineering Docs
Esc
navigateopen⌘Jpreview
On this page

Data

Postgres via Drizzle, how migrations work, and the tables you will actually touch.

Railway Postgres, accessed through Drizzle ORM, reached from Workers through Hyperdrive. The schema lives in lib/db/drizzle/schema.ts (49 tables), and data access is grouped by concern in lib/db/.

Migrations

Schema changes happen one way: a SQL file in db/migrations/, then bun run db:migrate. There are 133 of them. Drizzle is used as a query builder and type source, not as a migration generator.

bun run db:status          # what has run
bun run db:migrate         # apply pending
bun run db:sql "SELECT 1"  # ad-hoc query

Production migrations run through a GitHub Action on push to main. That workflow is dormant because main is dormant, but it is the launch-day migration path. Do not delete it.

Naming

camelCase in code, DTOs, and API payloads. snake_case only for actual database column names. The Drizzle schema is the boundary where the two meet.

The tables that matter

Table Holds
offer A product or offer, scraped and analyzed. Also carries offer_type (affiliate vs ecom_brand), product_disclosure, and brand_assets (real fonts, colours, type sheet, swipe refs)
persona Buyer personas, linked to an offer
outputs Every saved agent output, by module and offer
documents Editable long-form documents with revision support
campaigns, campaign_steps, campaign_messages The multi-stage campaign builder
Table Holds
chat_sessions One conversation
chat_messages Messages, with the content blocks in metadata.v2.blocks and a per-session sequence that orders them
background_jobs Anything asynchronous, surfaced in the notification bell
Table Holds
images, videos Generated assets, their status, and their R2 location
image_queue, video_queue In-flight generation tasks
static_ad_batches One ad batch: mode and disclosure frozen at concept time, concepts_json, approved_concepts, plan_json
image_folders Media library folders, shared across media types
user_uploads Files the user uploaded, including chat drops
characters A character identity plus its generated character map
elements Locations, props, and products used as visual references
storyboards, video_pipelines Scene-by-scene production state
image_presets Style presets, admin-managed
Table Holds
user People. clerk_user_id joins to Clerk, which owns identity only
admin_session, admin_verification Elevated admin actions
prompts The runtime system and user prompt per module
ai_models Chat, image, and video models with a JSONB config
app_settings Key/value settings, including the default model per type
subscription_plans, pending_customers Polar billing
api_request_logs, api_usage_logs Every external call, with agent attribution
email_templates, page_templates, themes Admin-editable content

The module table is not in the Drizzle schema file by that name but is central: it holds one row per AI module, keyed to lib/registry.ts. See Prompts.

Media storage

One R2 bucket, adstack-media, served at media.tryadstack.com, shared by staging and production.

Request and usage logging

Every external call is logged to api_request_logs with provider, agent attribution, status, timing, and sanitized payloads. Providers are cloudflare_ai_gateway, firecrawl, and kie_ai. The agent column identifies the caller: host, a specific agent slug, image-prompt, output-verifier, and so on. The admin UI is at /admin/logs.

sanitizeApiLogValue() redacts API keys, tokens, passwords, signed URLs, and data URLs before anything is written, truncates strings over 12K characters, and stops at depth 8.

Last updated on August 6, 2026

Was this page helpful?