Data

Morpheus works across two data planes, and knowing which one a rule reads from explains most of its behaviour.

Plane Store Role

OLTP

PostgreSQL (Supabase)

The operational database, shared with the Payability web application. Every write Morpheus makes to its own state lands here.

OLAP

BigQuery

A periodic export of the HubSpot portal plus a mirror of application tables. Every re-engagement segment and priority score is computed here.

Re-engagement handlers read from BigQuery and write to HubSpot; PostgreSQL is consulted only to resolve identity and ownership. BigQuery is an export, so a segment can lag reality by the export interval — a contact who booked a deal five minutes ago may still appear in a segment.

Pages

Page Contents

Domain model

The core entities and how they relate

ORM models

The 13 SQLAlchemy models and their conventions

Repositories

Every repository method, with signatures

Enumerations

All 20 StrEnum mirrors of the database enums

Runtime state in configs

Watermarks, cursors, and kill switches

BigQuery warehouse

Datasets, export shape, and deduplication

BigQuery views

All 11 views, with purpose and consumer

Prisma schema reference

The full authoritative schema

Schema ownership

The PostgreSQL schema is owned by the web application’s Prisma schema, not by this repository. There are no migrations here.

Consequences:

  • A column Morpheus needs must be added to the Prisma schema first, migrated there, and only then mapped in morpheus_data_layer.

  • The ORM mirrors the schema but does not define it. ForeignKey("subscriptions.id") is deliberately not declared — that table is not managed here and the constraint is enforced at the database level only.

  • Attribute names are camelCase and match the column exactly, so a model is a faithful, greppable reflection of the schema.

The current schema is reproduced verbatim at Prisma schema reference.

Tables Morpheus writes

Table Written by

sellers

enrichment-scorer (upsert, enrichment, score), morpheus-hubspot (hubspotCompanyId)

contacts

enrichment-scorer, enrichment-scraper (BigQuery sync), morpheus-hubspot (ids, lifecycle, heat)

target_emails

enrichment-scraper (BigQuery sync), morpheus-hubspot (opt-out webhook)

target_phone_numbers

enrichment-scraper (BigQuery sync), phone-agent (Twilio intel), morpheus-hubspot (Five9)

Call

morpheus-hubspot (dialer sync, Five9 sync)

CampaignTarget

morpheus-hubspot (engagement id, sync status, FK repair)

Activity

morpheus-hubspot (engagement sync, task logs), phone-agent (reminders), enrichment-scraper (lifecycle notes)

follow_up_tasks

morpheus-hubspot (create, status), phone-agent (escalation)

sales_reps

morpheus-hubspot (hubspotCallSegmentId)

configs

enrichment-scraper (cursor), morpheus-hubspot (watermarks), round-robin (cursor)

email_campaign_targets

email-agent (categorisation)

emails

email-agent (category)

Everything else in the schema — campaigns, sequences, phone numbers, domains, AI agent configuration, profiles — belongs to the web application. Morpheus reads some of it and writes none of it.

Tenancy

Every domain table carries subscriptionId. Production runs a single subscription, b191a905-5d19-4fa0-941b-d8eb81c0c477. Four of the five functions read it from an environment variable and reject mismatched events; email-agent derives it from the row it loaded. See tenant isolation.

Connection handling

All five Lambdas call init(database_url, pool_size=0) at cold start, which selects SQLAlchemy’s NullPool, and the connection string must be the Supabase Transaction mode pooler URL (port 6543). The reasoning is in pooling — it matters enough that getting it wrong exhausts the database’s connection limit under concurrency.