Skip to content

Architecture

SuperElectron edited this page Aug 21, 2026 · 3 revisions

Architecture

Single Rust binary, three run modes: serve (HTTP), worker (jobs), cli (operator commands).

Stack

Layer Tech Why
HTTP: webhooks, health, control API axum harness (Claude Code / OpenClaw) drives system through control API
Jobs: queues, cron, retries, concurrency apalis on Postgres replaces Trigger.dev; no extra infra beyond Postgres. Queue lives in apalis/apalis_meta schemas (search_path pinned) so its migrations never touch public._sqlx_migrations. Side effect: apalis installs pgcrypto with its functions in apalis_meta — pgcrypto is database-global, so an app migration adding CREATE EXTENSION IF NOT EXISTS pgcrypto is a no-op and its functions are not on public's path
Operational store + audit Postgres (sqlx, migrations in repo) contacts, companies, outreach_log, web_research, audit_history, capacity
Semantic memory Mem0 self-hosted (Apache-2.0, REST) replaces Personize: extraction (add), recall (search), digest. Its LLM = local gpt-oss
LLM OpenAI-compatible client → gpt-oss-120B on DGX provider swappable via env
Structured output serde + schemars typed LLM responses, parse + repair
Governance own module policy docs (ICP, brand voice, playbook, signal defs) + trigger keywords → injected into prompts

Deploy

docker compose up: app + postgres(+pgvector) + mem0. Runs anywhere. Zero external platforms.

Integrations (final scope, decided 2026-08-20)

Area In Cut
CRM HubSpot, CSV import Salesforce, Clay
Email send Gmail (multi-sender rotation), SendGrid Smartlead, raw SMTP
Reply monitoring Gmail API polling IMAP
Social LinkedIn via HeyReach (webhook events in)
Voice whole channel (Vapi/Bland/ElevenLabs)
Prospect data Apollo (discover/enrich), Tavily (web research)
Notifications Slack (alerts, digests, error handler)

Module map

src/domain/      types: contact, company, engagement, signal, task
src/config/      env, targeting (ICP), cadence, messaging, agent modes
src/db/          repositories + audit + capacity + workspace state
src/llm/         client, schemas, output, governance, multi-step composition
src/memory/      mem0: add / search / token-budgeted digest
src/clients/     apollo, tavily
src/connectors/  trait + gmail, sendgrid, slack, hubspot, heyreach
src/workflows/   sync, discovery, research, accounts, outreach, analysis, reporting, tasks
src/jobs/        apalis registrations + cron schedules
src/http/        webhooks router (handler registry), control API
src/cli/         serve, worker, job run, status, tail, gmail-auth, sync-csv

Data flow

  1. Sync (HubSpot pull / CSV) → Postgres + Mem0 ingest
  2. Discovery/enrichment (Apollo) → new contacts/companies
  3. Research (Tavily) + signal detection → scores, memories
  4. Account strategy → preflight → generation (recall + governance + LLM) → send (Gmail/SendGrid/HeyReach) with capacity limits, send windows, opt-out
  5. Inbound: Gmail reply polling + HeyReach webhooks → classify → act (notify Slack, log HubSpot, stop sequence)
  6. Reporting: daily digest, weekly report → Slack

Setup (dev)

  1. cp .env.example .env — fill keys (Apollo, Tavily, HubSpot, Gmail OAuth, SendGrid, HeyReach, Slack webhook, LLM endpoint)
  2. docker compose up -d postgres mem0
  3. cargo run -- cli gmail-auth (once)
  4. cargo run -- serve + cargo run -- worker

Security notes

  • Secrets in .env, never committed. Post-v1 option: jentic-one credential broker in front of connectors (transport-pluggable trait designed for it).
  • Audit table records property changes with confidence + updatedBy.

See Home, Development-Plan.

Dedupe migrations

Copy migration 0011 (email normalization), not 0005. 0011 merges: children re-point to the winner before any delete, terminal status (opted_out, disqualified) propagates onto the survivor, multiple sequence states collapse to the furthest-along, and losers go to a backup table. 0005 deletes with cascades — it shipped before the pattern existed and only ever ran on empty or junk data. In 0011, DROP INDEX and CREATE UNIQUE INDEX IF NOT EXISTS must stay adjacent: the IF NOT EXISTS is only safe because the DROP guarantees absence.

Clone this wiki locally