{
  "id": "2026-04-24-crm-design-02967b1939",
  "scope": "redkey",
  "source_of_truth": "repo",
  "source_path": "docs/specs/2026-04-24-crm-design.md",
  "source_kind": "markdown",
  "visibility": "internal",
  "renderer_id": "design_doc.dreamborn-forge.generated.v1",
  "design_system": "dreamborn-design-system:forge",
  "generated_at": "2026-05-09T13:00:55.669Z",
  "artifact_type": "design_doc",
  "schema_version": "design_doc.generated.v1",
  "title": "RedKey CRM — Design Spec",
  "summary": "RedKey CRM — Design Spec Date: 2026 04 24 Status: Design approved, awaiting implementation plan Scope: BezelIQ internal CRM (first), client deployment pattern (follow on) Overview RedKey CRM is company infrastructure, not a SaaS subscription. Contacts, deals, and activities live in Supabase. Agents do all operational work. Justin only provides strategic inpu...",
  "format_source": "markdown",
  "sections": [
    {
      "title": "RedKey CRM — Design Spec",
      "level": 1,
      "body": "**Date:** 2026-04-24\n**Status:** Design approved, awaiting implementation plan\n**Scope:** BezelIQ internal CRM (first), client deployment pattern (follow-on)\n\n---"
    },
    {
      "title": "Overview",
      "level": 2,
      "body": "RedKey CRM is company infrastructure, not a SaaS subscription. Contacts, deals, and activities live in Supabase. Agents do all operational work. Justin only provides strategic input. The CRM gets smarter as agents run — no manual data entry, no per-client licensing, no external dependency beyond Resend for email sending.\n\nTwenty CRM is used as the UI bootstrap for BezelIQ. Its data model is adopted as the schema foundation. The Twenty application is dissolved over time as the RedKey cockpit grows to cover the same views.\n\n---"
    },
    {
      "title": "Architecture",
      "level": 2,
      "body": "```\nTwenty full app (Docker on VPS)\n        ↓ connects to\nRedKey Supabase (Twenty's migrations applied)\n        ↓\nStable views (crm_contacts, crm_deals, crm_activities...)\n        ↓\n┌───────────────────────┬──────────────────────┐\n↓                       ↓                      ↓\nRedKey agents      Cockpit UI              Twenty UI\n(read/write views) (grows over time)      (used now, dissolves as cockpit grows)\n```\n\n**Single source of truth:** RedKey Supabase. Agents read and write directly via stable views. Any UI — Twenty or cockpit — is a view layer. No sync, no drift.\n\n**Dissolution path:**\n1. Launch: Twenty UI covers everything, cockpit covers nothing\n2. Build cockpit pipeline view → retire Twenty's pipeline view\n3. Build cockpit contact view → retire Twenty's contact view\n4. Build cockpit activity feed → retire Twenty's activity timeline\n5. Twenty app retired when cockpit coverage is complete\n\n---"
    },
    {
      "title": "Layer 1 — Twenty's tables (their migrations)",
      "level": 3,
      "body": "Twenty's app runs its own migrations against Supabase. These are Twenty's tables, Twenty's names. Agents never query these directly.\n\n| Table | What it is |\n|---|---|\n| `people` | Contacts — name, email, phone, LinkedIn |\n| `companies` | Companies / accounts |\n| `opportunities` | Deals — stage, amount, close date |\n| `pipeline_stages` | Configurable stages per pipeline |\n| `activities` | Emails, calls, meetings logged |\n| `notes` | Notes attached to any record |\n| `attachments` | Files |\n\nTwenty's `metadata` schema (custom objects/fields system) is left untouched."
    },
    {
      "title": "Layer 2 — Stable views (our abstraction)",
      "level": 3,
      "body": "Created immediately on top of Twenty's tables. Agents only ever touch these — never Twenty's raw tables. When Twenty upgrades and renames a column, the view is fixed — agents are unaffected.\n\n```sql\ncrm_contacts    → people          (filter by workspace_id, expose as client_id)\ncrm_companies   → companies       (filter by workspace_id, expose as client_id)\ncrm_deals       → opportunities   (filter by workspace_id, expose as client_id)\ncrm_activities  → activities      (filter by workspace_id, expose as client_id)\ncrm_pipeline    → pipeline_stages + pipelines (read-only join view)\n```\n\n**Client scoping:** Twenty uses `workspace_id` for multi-tenancy. Each BezelIQ workspace maps to one `client_id`. Stable views filter by `workspace_id` and expose it as `client_id` for consistency with the rest of RedKey. BezelIQ's CRM data and client CRM data never mix.\n\n**View writability:** Single-table views (`crm_contacts`, `crm_companies`, `crm_deals`, `crm_activities`) are directly updatable in Postgres — arlo writes to them like any table. Join views (`crm_pipeline`) are read-only; pipeline mutations go through a Supabase function rather than a direct view write."
    },
    {
      "title": "Strategic vs operational field separation",
      "level": 3,
      "body": "Strategic fields are human-owned. Operational fields are agent-owned. Neither crosses into the other's territory — enforced at the schema level.\n\n```sql\n-- Operational fields (arlo-owned, never human-edited)\nlast_activity_at, enriched_at, email_thread_count, open_count,\nlinkedin_url, company_size, industry, sequence_step, sequence_enrolled_at\n\n-- Strategic fields (Justin-owned, never agent-overwritten)\nstage_id, priority, strategy_note, relationship_note, recontact_at\n```"
    },
    {
      "title": "Sequence tables (new, not in Twenty)",
      "level": 3,
      "body": "```sql\ncrm_sequences         — sequence definition (name, steps as jsonb, client_id)\ncrm_sequence_contacts — enrollment: contact_id, sequence_id, current_step,\n                        next_due_at, status (active/complete/paused), client_id\n```\n\n---"
    },
    {
      "title": "Arlo — the CRM agent",
      "level": 3,
      "body": "**Agent ID:** `arlo`\n**Role topic:** `roles.crm`\n**Model:** Sonnet\n**Timer:** 30s (same as Engine)\n\nArlo handles all CRM work. Engine creates tasks, arlo claims and executes, posts results. Standard RedKey pattern — nothing novel.\n\n**Task types:**\n\n| Task | Trigger | What arlo does |\n|---|---|---|\n| Contact enrichment scan | Engine timer (hourly) | Scans `crm_contacts` for unenriched records, calls enrichment APIs, writes operational fields back |\n| Pipeline health check | Engine timer (daily) | Flags deals with no activity past threshold, posts summary to `roles.exec` |\n| Sequence step | Engine timer (30s) | Checks `crm_sequence_contacts` for `next_due_at` past due, drafts + sends via Resend, logs to `crm_activities`, advances `next_due_at` |\n| Strategic CRM write | Justin via Atlas | Atlas creates a task on `roles.crm` from conversation input — arlo writes to strategic fields only |\n| Ad-hoc CRM query | Justin via Atlas | Summarise pipeline, draft follow-up, flag contacts — any one-off work |"
    },
    {
      "title": "How arlo reads and writes",
      "level": 3,
      "body": "Direct Supabase client against stable views. No MCP server needed for data access. Resend is registered in `api_library` — arlo calls the Resend API for all outbound email.\n\n---"
    },
    {
      "title": "CRM Write Model",
      "level": 2,
      "body": "**The core principle:** Justin never manually updates operational data. The only input he provides is strategic judgment and relationship context — things an agent cannot supply."
    },
    {
      "title": "Operational data — automated",
      "level": 3,
      "body": "All contact data, activity history, email threads, meeting logs, enrichment, sequence tracking, and email engagement metrics flow in automatically from the ingestion layer.\n\n| Source | Connection | What arlo writes |\n|---|---|---|\n| Email | Twenty's native Gmail sync | Activities, contact discovery, thread history |\n| Calendar | Twenty's native calendar sync | Meetings logged, attendees linked to deals |\n| Quill meeting notes | Quill webhook → edge function → HCS → arlo | Notes on contact/deal, next steps extracted |\n| Website forms | Form POST → Supabase directly | New contact record, source tagged |\n| Resend | Resend webhook → HCS → arlo | Email sent/opened/clicked logged to `crm_activities` |\n| LinkedIn enrichment | Arlo timer (hourly) | Company, role, LinkedIn URL, headcount written to operational fields |"
    },
    {
      "title": "Strategic data — Justin via Atlas",
      "level": 3,
      "body": "Justin never opens a form. He tells Atlas in conversation:\n\n> *\"The Acme deal is stalled — budget freeze until Q3, recontact in July\"*\n> *\"Mark Sarah at Meridian as a priority — strong champion, CFO relationship\"*\n> *\"Move the Volta deal to Negotiation, we're close\"*\n\nAtlas creates a task on `roles.crm`. Arlo writes the strategic fields. Justin's input becomes a structured CRM record without him touching the UI.\n\n---"
    },
    {
      "title": "Email Stack",
      "level": 2,
      "body": "**Resend** is the sending infrastructure for all outbound CRM email.\n\n- Verify `bezeliq.com` (or `mail.bezeliq.com` for reputation isolation) in Resend\n- Set DNS once: SPF, DKIM, DMARC\n- All outbound — sequences and broadcasts — goes through Resend from `@bezeliq.com`\n- Resend handles deliverability, bounces, unsubscribes, open tracking\n- Resend webhooks post events back to an edge function → HCS → arlo logs to `crm_activities`\n- Resend registered in `api_library` for resolver to find on CRM intent\n\n**Domain warm-up:** warm `bezeliq.com` before sequences start at volume. Low-volume early-stage sending (BezelIQ's current state) is fine without warmup.\n\n---"
    },
    {
      "title": "Client Deployment Pattern",
      "level": 2,
      "body": "By the time RedKey deploys company infrastructure for clients, the cockpit has enough CRM coverage that clients never need to see Twenty. Clients get the RedKey-native CRM from day one.\n\n**Client onboarding sequence:**\n1. CRM migrations run against client's Supabase (same tables, same views)\n2. Arlo deployed with `client_id` scope\n3. Cockpit CRM views provisioned\n4. Client ingestion sources connected (their Gmail, calendar, forms, meeting notes)\n5. Client's sending domain verified in Resend\n\n**Full stack per client:**\n\n| Layer | What | Hosted where |\n|---|---|---|\n| Data | CRM tables + stable views | Client's Supabase |\n| Agent | Arlo (client_id scoped) | VPS (shared runner) |\n| Email | Resend (client's domain) | Resend (SaaS, per domain) |\n| UI | Cockpit CRM views | Client's cockpit deployment |\n| Ingestion | Gmail sync, calendar, forms, Quill | Connected at onboarding |\n\n**The product value:** clients get a CRM that is owned infrastructure (no SaaS subscription), natively integrated with their agent workflows, and zero manual data entry from day one. Every agent run teaches the system. Every deal, every contact, every activity is captured automatically.\n\n---"
    },
    {
      "title": "Open Questions",
      "level": 2,
      "body": "- **Quill API/webhook:** confirm Quill supports outbound webhooks or API for meeting note retrieval before designing the ingestion connector\n- **Twenty → Supabase compatibility:** test Twenty's Docker Compose with `PG_DATABASE_URL` pointed at Supabase before committing — RLS defaults and connection pooling (pgBouncer) may need configuration\n- **Arlo enrichment APIs:** choose enrichment provider (Clearbit, Apollo, People Data Labs) — evaluate cost, data quality, API availability. Clearbit is being absorbed into HubSpot which may affect availability.\n- **Sequence reply detection:** Resend webhooks cover opens/clicks but not replies. Reply detection requires Gmail API polling or a dedicated inbound email handler (e.g., Resend inbound, Postmark inbound).\n- **Broadcast vs sequence distinction in schema:** `crm_sequences` covers 1:1 sequences. Broadcast campaigns (mass email to a list) may need a separate `crm_campaigns` table — out of scope for v1 but worth noting."
    }
  ],
  "html_path": "artifacts/2026-04-24-crm-design-02967b1939.html",
  "json_path": "artifacts/2026-04-24-crm-design-02967b1939.json"
}