Schema Naming Contracts

This document is contractual. Future schema or API changes must comply with it.

1. Purpose & Scope

What This Contract Governs

  • PostgreSQL table, column, enum, index, constraint, and function names
  • TypeScript type and interface property names
  • Zod schema names and field keys
  • API endpoint paths and payload keys
  • Queue message types and payload keys
  • File and directory naming conventions

What This Contract Does NOT Govern

  • UI display labels (e.g., "Check Frequency" vs "check_frequency")
  • Marketing copy and brand names
  • Analytics event names
  • Log message formatting
  • Third-party API integrations (Stripe, Supabase Auth, etc.)

2. Canonical Naming Rules (by Layer)

PostgreSQL

ElementConventionExample
Tablessnake_case (plural preferred)tracked_urls, url_checks
Columnssnake_caseuser_id, created_at
Enum typessnake_casebrand_url_type
Enum valuessnake_case or SCREAMING_SNAKE_CASE'daily', 'DOMAIN_MISSING'
Indexesidx_{table}_{columns}idx_tracked_urls_user
Constraints{table}_{description}url_checks_one_per_day
Foreign keys{table}_{column}_fkeytracked_urls_user_id_fkey
Triggers{table}_{action}profiles_updated_at
Functionssnake_caseupdate_updated_at()

TypeScript

ElementConventionExample
Interface namesPascalCaseTrackedUrl, UrlCheck
Type aliasesPascalCaseCheckFrequency, BrandUrlType
Interface propertiessnake_caseuser_id, created_at
VariablescamelCasetrackedUrl, userId
ConstantsSCREAMING_SNAKE_CASEPOLL_INTERVAL_MS
FunctionscamelCasegetNextCheckTime()

Important: TypeScript interface properties use snake_case to maintain 1:1 mapping with PostgreSQL columns. This eliminates transformation overhead and reduces errors.

Zod Schemas

ElementConventionExample
Schema variablePascalCase + Schema suffixCreateTrackedUrlSchema
Enum schemaPascalCase + Schema suffixCheckFrequencySchema
Object keyssnake_casetracked_url_id, enqueued_at

API Routes & Payloads

ElementConventionExample
Endpoint pathskebab-case or abbreviated/api/urls, /api/enqueue-due-urls
Route parameters[id] (Next.js convention)/api/urls/[id]
Request body keyssnake_case{ user_note: "..." }
Response body keyssnake_case{ created_at: "..." }
Query parameterssnake_case?is_active=true

Queues / Workers

ElementConventionExample
Message typeSCREAMING_SNAKE_CASETRACK_URL, RENDER_URL
Payload keyssnake_casetracked_url_id, url
Metadata keyssnake_caseenqueued_at, attempt
Queue namessnake_case with colon prefixqueue:track_url

Files & Directories

ElementConventionExample
TypeScript fileskebab-case.tscontent-normalizer.ts
Test files{name}.test.tsutils.test.ts
React componentsPascalCase.tsxTrackedUrlCard.tsx
Documentationkebab-case.mdurl-normalization.md
Migrations{nnnn}_{description}.sql0006_v1_contracts.sql
Config filesstandard or kebab-casebiome.json, tsconfig.json

3. Locked Identifiers (DO NOT RENAME)

WARNING: Changing any of the following requires:

  1. A database migration with backfill
  2. Contract version bump
  3. Review by db-migration-operator

Tables (32)

Core Tables (V1)

TablePurpose
profilesUser profiles synced from Supabase Auth
tracked_urlsURLs being monitored by users (V1)
url_checksIndividual check records (V1)
url_changesChange records with diff metadata (V1)
summariesAI-generated change summaries (V1)
url_rendersPlaywright render artifacts (V1)

V1 Contract Tables

TablePurpose
plan_definitionsData-driven plan configuration
user_subscriptionsMaps users to plans
notification_preferencesPer-user notification settings
notification_deliveriesAudit log for sent notifications
tracking_suggestionsDiscovery templates
tracking_suggestion_examplesURL type examples per suggestion
brand_sitesDomain grouping for tracked URLs

Brand Library Tables

TablePurpose
brand_library_sourcesProvenance tracking
brand_library_brandsCanonical brand entities
brand_library_urlsValidated trackable URLs
brand_library_review_queueAmbiguity/low-confidence items

vNext Global URL Library Tables

TablePurpose
urlsGlobal URL library (unique by canonical_url)
url_observationsGlobal check records (one per URL per day)
url_change_eventsGlobal change records
url_change_summariesGlobal AI summaries
url_render_artifactsGlobal render artifacts
user_url_libraryUser-URL relationship with history gating
url_policy_decisionsURL policy audit log
url_submissionsURL submission workflow
trend_definitionsTrend configuration
trend_pointsTrend time-series data
user_profile_detailsProfile extension (display_name, timezone)
onboarding_stateOnboarding progress tracking
billing_customersStripe customer mapping
invoicesInvoice records
billing_webhook_eventsStripe webhook audit log

Critical Columns

Table.ColumnTypeLocked Reason
tracked_urls.canonical_urlTEXTUsed for uniqueness constraint
tracked_urls.check_frequencyTEXTCHECK constraint: 'daily', 'weekly'
tracked_urls.user_noteTEXTUser-facing field
url_checks.content_hashTEXTUsed for change detection
url_checks.has_changeBOOLEANUsed in indexes
url_checks.checked_dayDATEGenerated column, unique constraint
url_checks.section_hashesJSONBStructured as [{id, hash, heading?}]
summaries.confidenceNUMERIC(3,2)Range: 0.00-1.00
summaries.ai_providerTEXTProvider identifier
summaries.ai_modelTEXTModel identifier
plan_definitions.plan_keyTEXTPrimary key, FK reference
plan_definitions.max_tracked_urlsINTEGERUsed for URL cap enforcement
plan_definitions.allowed_cadencesTEXT[]Array of allowed frequencies
user_subscriptions.statusTEXTCHECK: 'active', 'canceled', 'past_due'
notification_deliveries.channelTEXTCHECK: 'email'
notification_deliveries.statusTEXTCHECK: 'queued', 'sending', 'pending', 'sent', 'failed'
notification_deliveries.dedupe_keyTEXTNullable; partial unique index
brand_sites.canonical_domainTEXTUNIQUE constraint for domain dedup
brand_sites.sourceTEXTCHECK: 'user_added', 'brand_seeded', 'admin_added'
tracked_urls.brand_site_idUUIDFK to brand_sites (nullable until migration 0010)

PostgreSQL Enums (16)

Brand Library Enums

Enum TypeValues
brand_url_type'pricing', 'status', 'privacy', 'terms', 'security'
brand_confidence_level'high', 'medium', 'low'
brand_entity_type'brand', 'url'
brand_review_status'open', 'fixed', 'ignored'
brand_review_reason'DOMAIN_AMBIGUOUS', 'DOMAIN_MISSING', 'CONFLICTING_DOMAINS', 'SUMMARY_LOW_CONFIDENCE', 'SUMMARY_TOO_MARKETING', 'INSUFFICIENT_INPUTS', 'NO_MATCH_FOR_URL_TYPE', 'MULTIPLE_STRONG_CANDIDATES', 'OFF_DOMAIN_REDIRECT', 'URL_TYPE_MISMATCH'

vNext Enums

Enum TypeValues
url_source'brand_seeded', 'user_added', 'admin_added'
url_status'active', 'paused', 'blocked', 'needs_review'
url_type_extended'pricing', 'status', 'privacy', 'terms', 'security', 'changelog', 'docs', 'blog', 'other'
policy_decision'allow', 'block', 'review'
policy_reason_code'adult_content', 'hate_or_extremism', 'malware_or_phishing', 'spam_or_low_quality', 'illegal_or_regulated', 'auth_wall_or_uncheckable', 'duplicate', 'unknown'
policy_decider'system', 'admin'
submission_status'pending', 'blocked', 'approved_created_global', 'approved_linked_existing', 'needs_review'
trend_type'category_trend', 'brand_vs_brand', 'seasonal', 'cyclical', 'url_type_trend'
invoice_status'draft', 'open', 'paid', 'uncollectible', 'void'
webhook_event_status'received', 'processed', 'failed'
onboarding_step'welcome', 'add_first_url', 'explore_brand_library', 'configure_notifications', 'completed'

vNext Critical Columns

Table.ColumnTypeLocked Reason
urls.canonical_urlTEXTUNIQUE constraint, global deduplication
urls.sourceurl_sourceTracks URL provenance
urls.statusurl_statusControls tracking state
url_observations.checked_dayDATEGenerated column, unique constraint
url_change_events.url_idUUIDFK to global urls
url_change_summaries.url_change_event_idUUIDUNIQUE FK to events
user_url_library.history_start_atTIMESTAMPTZNULL = full history, set = gated
user_url_library.user_id + url_id-Unique constraint pair
url_submissions.statussubmission_statusWorkflow state
url_policy_decisions.decisionpolicy_decisionAllow/block/review
billing_customers.stripe_customer_idTEXTUNIQUE, Stripe integration
invoices.stripe_invoice_idTEXTUNIQUE, Stripe integration

API Endpoints (3)

MethodPathPurpose
GET/POST/api/urlsList/create tracked URLs
GET/PATCH/DELETE/api/urls/[id]Read/update/delete tracked URL
POST/api/enqueue-due-urlsCron endpoint for queue
GET/api/brandsList user's brand sites
GET/api/brands/[id]Get brand site with tracked URLs

Queue Message Types (2)

TypePurpose
TRACK_URLFetch and check URL for changes
RENDER_URLPlaywright render request

Notification Delivery Status (Resolved Drift)

As of migration 0008_notification_dedupe_key.sql, the database column notification_deliveries.status now includes all 5 states:

'queued' | 'sending' | 'pending' | 'sent' | 'failed'

State definitions:

  • pending — Initial state, awaiting processing (database DEFAULT)
  • queued — Scheduled for delivery
  • sending — Delivery in progress
  • sent — Successfully delivered
  • failed — Delivery failed (see error_message)

The previous drift between database (3 states) and application code (5 states) has been resolved. All states are now canonical and persisted.

History: Prior to migration 0008, the database CHECK constraint only allowed 'pending', 'sent', 'failed'. States 'queued' and 'sending' existed only at runtime. This was intentionally expanded in 0008 to support proper delivery tracking.


4. Cross-Layer Mapping Rules

All layers use snake_case for data properties. Mapping is 1:1 (no transformation).

DB ColumnTS PropertyAPI FieldNotes
user_iduser_iduser_idUUID reference to profiles
created_atcreated_atcreated_atISO 8601 string
check_frequencycheck_frequencycheck_frequency'daily' or 'weekly'
what_changedwhat_changedwhat_changedSummary field
tracked_url_idtracked_url_idtracked_url_idFK to tracked_urls
content_hashcontent_hashcontent_hashSHA-256 hash
section_hashessection_hashessection_hashesSectionMeta[]

Transformation rule: None. DB value flows unchanged through TS types to API payloads.


5. Agent Enforcement Notes

Ownership

  • db-migration-operator owns all schema naming decisions
  • architect-steward enforces contract compliance during PR review

Agent Behavior Requirements

  1. Agents MUST refuse to rename locked identifiers
  2. Agents MUST use snake_case for all new columns and TS properties
  3. Agents MUST use SCREAMING_SNAKE_CASE for new enum values when adding to brand_review_reason
  4. Agents MUST use snake_case for new enum values when adding to other enums
  5. New tables MUST follow {domain}_{entity} pattern for namespacing
  6. New API endpoints MUST use snake_case for all request/response keys

Adding New Identifiers

When adding a new column:

-- CORRECT
ALTER TABLE tracked_urls ADD COLUMN last_error_message TEXT;

-- INCORRECT (camelCase)
ALTER TABLE tracked_urls ADD COLUMN lastErrorMessage TEXT;

When adding a new TypeScript property:

// CORRECT
interface TrackedUrl {
  last_error_message: string | null
}

// INCORRECT
interface TrackedUrl {
  lastErrorMessage: string | null
}

6. Non-Goals

This contract explicitly does NOT govern:

  1. UI copy/labels - The UI may display "Check Frequency" for check_frequency
  2. Marketing names - "Eko Pro" is a display name, plan_key: 'pro' is the identifier
  3. Analytics events - External analytics may use different conventions
  4. Third-party integrations - Stripe webhook payloads, Supabase Auth responses, etc.
  5. Log message formatting - Logger output is not bound by these rules
  6. Internal variable names - Local variables in functions may use camelCase
  7. React component props - Component-internal props may use camelCase

Appendix: Common Abbreviations

AbbreviationFull FormUsage
urlUniform Resource LocatorAlways lowercase
idIdentifierAlways lowercase
msMillisecondsSuffix for time values
fkForeign KeyIn constraint names
pkPrimary KeyIn documentation only
dlqDead Letter QueueQueue suffix