RedKey CRM — Design Spec
internal prototype · canonical JSON + Dreamborn Forge HTML
internal generated
design_doc · markdown

RedKey CRM — Design Spec

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...

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 input. The CRM gets smarter as agents run — no manual data entry, no per-client licensing, no external dependency beyond Resend for email sending.

Twenty 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.

---

Architecture

`` Twenty full app (Docker on VPS) ↓ connects to RedKey Supabase (Twenty's migrations applied) ↓ Stable views (crm_contacts, crm_deals, crm_activities...) ↓ ┌───────────────────────┬──────────────────────┐ ↓ ↓ ↓ RedKey agents Cockpit UI Twenty UI (read/write views) (grows over time) (used now, dissolves as cockpit grows) ``

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.

Dissolution path: 1. Launch: Twenty UI covers everything, cockpit covers nothing 2. Build cockpit pipeline view → retire Twenty's pipeline view 3. Build cockpit contact view → retire Twenty's contact view 4. Build cockpit activity feed → retire Twenty's activity timeline 5. Twenty app retired when cockpit coverage is complete

---

Layer 1 — Twenty's tables (their migrations)

Twenty's app runs its own migrations against Supabase. These are Twenty's tables, Twenty's names. Agents never query these directly.

| Table | What it is | |---|---| | people | Contacts — name, email, phone, LinkedIn | | companies | Companies / accounts | | opportunities | Deals — stage, amount, close date | | pipeline_stages | Configurable stages per pipeline | | activities | Emails, calls, meetings logged | | notes | Notes attached to any record | | attachments | Files |

Twenty's metadata schema (custom objects/fields system) is left untouched.

Layer 2 — Stable views (our abstraction)

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.

``sql crm_contacts → people (filter by workspace_id, expose as client_id) crm_companies → companies (filter by workspace_id, expose as client_id) crm_deals → opportunities (filter by workspace_id, expose as client_id) crm_activities → activities (filter by workspace_id, expose as client_id) crm_pipeline → pipeline_stages + pipelines (read-only join view) ``

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.

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.

Strategic vs operational field separation

Strategic fields are human-owned. Operational fields are agent-owned. Neither crosses into the other's territory — enforced at the schema level.

```sql -- Operational fields (arlo-owned, never human-edited) last_activity_at, enriched_at, email_thread_count, open_count, linkedin_url, company_size, industry, sequence_step, sequence_enrolled_at

-- Strategic fields (Justin-owned, never agent-overwritten) stage_id, priority, strategy_note, relationship_note, recontact_at ```

Sequence tables (new, not in Twenty)

``sql crm_sequences — sequence definition (name, steps as jsonb, client_id) crm_sequence_contacts — enrollment: contact_id, sequence_id, current_step, next_due_at, status (active/complete/paused), client_id ``

---

Arlo — the CRM agent

Agent ID: arlo Role topic: roles.crm Model: Sonnet Timer: 30s (same as Engine)

Arlo handles all CRM work. Engine creates tasks, arlo claims and executes, posts results. Standard RedKey pattern — nothing novel.

Task types:

| Task | Trigger | What arlo does | |---|---|---| | Contact enrichment scan | Engine timer (hourly) | Scans crm_contacts for unenriched records, calls enrichment APIs, writes operational fields back | | Pipeline health check | Engine timer (daily) | Flags deals with no activity past threshold, posts summary to roles.exec | | 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 | | Strategic CRM write | Justin via Atlas | Atlas creates a task on roles.crm from conversation input — arlo writes to strategic fields only | | Ad-hoc CRM query | Justin via Atlas | Summarise pipeline, draft follow-up, flag contacts — any one-off work |

How arlo reads and writes

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.

---

CRM Write Model

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.

Operational data — automated

All contact data, activity history, email threads, meeting logs, enrichment, sequence tracking, and email engagement metrics flow in automatically from the ingestion layer.

| Source | Connection | What arlo writes | |---|---|---| | Email | Twenty's native Gmail sync | Activities, contact discovery, thread history | | Calendar | Twenty's native calendar sync | Meetings logged, attendees linked to deals | | Quill meeting notes | Quill webhook → edge function → HCS → arlo | Notes on contact/deal, next steps extracted | | Website forms | Form POST → Supabase directly | New contact record, source tagged | | Resend | Resend webhook → HCS → arlo | Email sent/opened/clicked logged to crm_activities | | LinkedIn enrichment | Arlo timer (hourly) | Company, role, LinkedIn URL, headcount written to operational fields |

Strategic data — Justin via Atlas

Justin never opens a form. He tells Atlas in conversation:

> *"The Acme deal is stalled — budget freeze until Q3, recontact in July"* > *"Mark Sarah at Meridian as a priority — strong champion, CFO relationship"* > *"Move the Volta deal to Negotiation, we're close"*

Atlas creates a task on roles.crm. Arlo writes the strategic fields. Justin's input becomes a structured CRM record without him touching the UI.

---