Skip to Content
DatabaseDatabase Overview

Database Overview

The system uses three storage layers. Each was chosen for a specific access pattern. This document explains the decision framework. For full schemas, see the dedicated docs.

DetailWhere
PostgreSQL schema — all tablespostgres.md
MongoDB collections — all collectionsmongo.md
SQL ER diagramsentity-relations-sql.md
NoSQL collection cross-referencesentity-relations-nosql.md

Which Database for What

Rule of thumb:

  • Relational data with counts, joins, or billing → PostgreSQL
  • Variable-structure metadata, logs, append-only operational data → MongoDB
  • Large binary files or large text blobs (PDFs, full article content, scraped text) → S3
DataStoreReason
Tenants, users, campaigns, billingPostgreSQLRelational, transactional, foreign keys matter
Strategies, goals, deliverablesPostgreSQLNeeds ACID guarantees across status transitions
Activities, activity runsPostgreSQLState machine with strict transitions; must be counted and queried
LLM cost audit (llm_calls)PostgreSQLAppend-only, needs aggregation queries for cost reporting and billing
Tool call summaries (tool_calls)PostgreSQLNeed to be counted per activity, joined to runs
Channels, channel scoresPostgreSQLRelational (FK from blog_posts, social_posts); score deltas need querying
Blog/social requests and entitiesPostgreSQLStatus machine, FK relationships, approval flow
BacklinksPostgreSQLNeeds counting, filtering by status, joining to activities
Leads, lead activitiesPostgreSQLCRM data — needs filtering, sorting, assignment
Paid campaigns, search terms, classificationsPostgreSQLNeeds HITL review workflow with status, push tracking
Sessions, agent configsPostgreSQLRelational config; session expiry queries
Contracts (acceptance record)PostgreSQLNeeds foreign key to users, queried in auth middleware; PDF key stored here
Skills contentMongoDBFrequently updated Markdown; needs full-text search for skill discovery
Agent output streaming bufferMongoDBWrite-heavy small deltas, variable structure, TTL cleanup
Activity/event logsMongoDBHigh-volume append, small payloads, TTL cleanup
Audit logsMongoDBCompliance; before/after diffs are arbitrary small JSON
Tool call full payloadsMongoDBJSONB varies per integration; typically small, 90d TTL
Deliverable content (blog posts, ad copy)S3Large text blobs (multi-KB Markdown); served via pre-signed URL
Contract PDFsS3Binary PDF; immutable legal record; Object Lock (WORM)
Onboarding researchS3Large scraped text (50–200 KB per field)
Strategy documentsS3Long-form Markdown narrative (2,000–10,000 words)

Split Pattern: PostgreSQL + MongoDB + S3

Large content is split across all three stores. PostgreSQL holds structured metadata; MongoDB holds variable-structure metadata and operational logs; S3 holds the actual content bytes.

EntityPostgreSQL holdsMongoDB holdsS3 holds
Activity outputStatus, output_ref (MongoDB ObjectId)Metadata + s3KeyFull content (Markdown/JSON)
Blog postMetadata (status, channel, word count)Full article Markdown (via output_refactivity_outputs.s3Key)
Social postMetadata (status, platform)Post copy + hashtags (via output_refactivity_outputs.s3Key)
Onboarding runStatus, who reviewed, context_file_idS3 key pointers (onboarding_research)Scraped text, competitor research, context file
StrategyVersion, status, dates, approvalS3 key pointer (strategies_content)Full strategy Markdown narrative
ContractAcceptance metadata, pdf_s3_keyGenerated PDF binary
Tool callSummary (name, method, status, duration)Full payloads (tool_call_logs)

Access pattern for large content:

  1. Fetch MongoDB or PostgreSQL metadata record (fast, indexed)
  2. Extract the s3Key
  3. Call s3.getSignedUrl(s3Key, { expiresIn: 900 }) for a 15-minute pre-signed URL
  4. Return URL to client; client fetches content directly from S3

Key Relationships (PostgreSQL)

tenants └── strategies (one active at a time) └── goals (multiple per strategy) └── deliverables (multiple per goal) └── deliverable_periods (one per month) └── activities (pipeline steps) └── activity_runs (execution attempts) └── llm_calls (cost audit) └── tool_calls (integration audit) └── [output_ref → MongoDB activity_outputs] tenants └── channels (OAuth-connected platforms) └── blog_requests → blog_posts (CMS content) └── social_media_requests → social_posts (social content) └── channel_scores (monthly health scores) └── ad_campaigns → ad_keywords → search_term_reports → search_term_classifications → ad_optimization_recommendations tenants └── leads → lead_activities └── backlinks └── agent_configs └── sessions └── recurring_task_templates (copied from global templates) └── activity_templates (copied from global templates)

Base Record Fields

All records — in both PostgreSQL and MongoDB — follow a standard set of base fields. See PostgreSQL Schema → Base Record Fields for the Prisma schema composition pattern.

Naming conventions (PostgreSQL):

  • All timestamp columns use _on suffix: created_on, updated_on, deleted_on
  • FK columns for user references use _by suffix: created_by, reviewed_by
  • External IDs use external_ prefix: external_campaign_id, external_keyword_id
  • ref_id is the ULID, used in APIs/URLs. id (UUID) is used only for internal FK joins.

Engines Summary

PostgreSQL 16MongoDB 7
ORM/ODMPrisma ORMMongoose
Packageproviders/provider-db/providers/provider-db/
ConnectionPOSTGRES_URL envMONGO_URL env
Multi-tenancytenant_id column on every tabletenantId field on every document
Soft deletesdeleted_on IS NULL filter{ deletedOn: null } filter
Base fieldsComposed via baseFields, mutableFields, softDeleteFieldsBaseDocument interface
Local devDocker Compose postgres:16Docker Compose mongo:7

© 2026 Leadmetrics — Internal use only