Skip to content

Repository files navigation

OpenRelay v2

A vertical operations engine where vertical behavior is data (the RelayPack, packs/*.pack.yaml) and — since v2 — integrations are data too (MCP capability bindings). Application code is a deterministic kernel (funnel, sequences, the action firewall, append-only audit) plus generic engines; grep -ri "lash" apps/ packages/ returns nothing (vertical content lives only in packs/ and test fixtures — the sole exception is the spec-pinned Appendix A enum in packages/db/migrations/001_core.sql).

The first shipped pack uses Resend, Gmail, Meta, Cal.com, and Stripe as default provider templates, not as product primitives. The product primitive is the capability slot (email.send, leads.fetch, calendar.booking.events, etc.) and the binding that fills it.

v2 makes OpenRelay both sides of the Model Context Protocol:

  • An MCP server — any MCP client (Claude Desktop/Code, the in-product /ask console, a test harness) operates the system through audited tools at POST :8787/mcp or the stdio entry (pnpm --filter @openrelay/worker mcp:stdio). Every tool routes through the kernel's guardrails; there is NO tool that delivers a message outside the action firewall, reads a credential, or runs raw SQL.
  • An MCP client — packs declare required capabilities (email.send, leads.fetch, ads.insights, …) and operators bind any MCP server that provides them on /setup (builtin, stdio command, or remote URL with OAuth 2.1). Defaults are builtin servers wrapping the v1 Resend/Gmail/Meta adapters — zero behavior change for the first tenant, and grep -rn "api.resend.com\|graph.facebook.com" hits only those builtins.

Docs: OPENRELAY_ARCHITECTURE.md is the complete architecture & handoff map (what's built, where everything lives, how to extend it). OPENRELAY_V2_MCP_SPEC.md is the design spec for the MCP platform layer. (The earlier v1/v1.1/v1.3 specs have been retired; their intent is captured in the architecture doc and in in-code spec-section references.)

Layout

apps/web         Next.js 15 + Tailwind v4 + shadcn/ui admin (dashboard, queue,
                 pipeline, clients, ops, setup, /ask agent console; sidebar
                 shell, dark mode, ⌘K) + public token-gated intake form /
                 client uploads. Better Auth + Google OAuth.
apps/worker      pg-boss queues + webhook server + CLIs + the OpenRelay MCP
                 server (/mcp + stdio) + builtin capability MCP servers
                 (mcp-servers/{resend,gmail,meta,ai}) + MCP client pool +
                 event-capability poller + agent loop
packages/db      SQL migrations + thin typed query layer (pg) + org context
packages/core    pack loader/interpreter, funnel machine, firewall, sequence
                 scheduler, classifiers, STL engine, report renderer,
                 capability registry, connections directory, organizations,
                 autonomy ladder
packs/           the RelayPack + email template bodies (ALL vertical content)

Getting started

pnpm install
cp .env.example .env            # fill in DATABASE_URL at minimum

# Postgres 16 (or: docker run -d -p 5432:5432 -e POSTGRES_USER=openrelay \
#   -e POSTGRES_PASSWORD=openrelay -e POSTGRES_DB=openrelay postgres:16)
pnpm build
pnpm db:migrate                 # migrations 001-012
pnpm pack:validate              # zod + semantic + template checks
pnpm pack:activate              # transactional: failure keeps previous active

pnpm import:contacts list.csv b2b_list_v1 lash_business   # P1 import
pnpm dev:worker                 # queues + webhooks + /mcp on :8787
pnpm dev:web                    # admin UI on :3000

Connect a local MCP client (Claude Desktop / Claude Code) to the running system via stdio:

{
  "mcpServers": {
    "openrelay": {
      "command": "node",
      "args": ["<repo>/apps/worker/dist/mcp/stdio.js"],
      "env": {
        "DATABASE_URL": "postgres://openrelay:openrelay@localhost:5432/openrelay",
        "MCP_ACTOR": "you@agency.test"
      }
    }
  }
}

Tests (spin up a disposable Postgres first; tests create throwaway databases):

docker run -d --name openrelay-pg -p 5433:5432 -e POSTGRES_USER=openrelay \
  -e POSTGRES_PASSWORD=openrelay -e POSTGRES_DB=openrelay postgres:16
TEST_DATABASE_URL=postgres://openrelay:openrelay@localhost:5433/openrelay pnpm test

Integration tests skip automatically when no test database is reachable.

Capability vocabulary

Packs declare the capabilities they need; operators bind those capabilities to MCP servers or use the shipped default templates. The currently implemented MVP slots are email.send, email.test, leads.fetch, ads.campaign, ads.insights, calendar.link, leads.events, and reserved sms.send (cap remains 0 until provenance is documented).

The v2.1 taxonomy also reserves generic business primitives for future packs: crm.contact.upsert, calendar.booking.events, payments.events, forms.submit, chat.send, ads.lead_source, report.metric.fetch, ai.chat.complete, ai.generate.structured, and ai.agent.turn. They are valid pack vocabulary now, but they do not appear in /setup until a pack actually requires them and a server/template provides them.

Ingress rule: MCP is the capability layer for effectors and long-tail polling. Native signed webhooks remain first-class for urgent/trusted ingress where provider signatures and latency are better, and MCP *.events capabilities cover sources that do not provide suitable webhooks.

How it fits together

Loop A (agency growth). CSV import lands contacts at the pack's initial funnel stage with consent unknown — they cannot be messaged until a written attestation upgrades segments to granted (consent_events.evidence_url). The sequence scheduler enrolls pack audiences, walks steps, and sends through the action firewall — the only send path in the system: suppression → consent → quiet hours (marketing only) → daily cap + warmup ramp, every decision audited. Replies arrive via Gmail Pub/Sub push, get intent-classified (Haiku; below the pack confidence threshold → human review), drive the funnel machine, and confident interested/question replies get an Opus-drafted reply into the human queue. Cal.com bookings flip the funnel to call_booked; a purchase webhook creates the client and starts onboarding.

Loop B (client delivery). Onboarding walks the pack steps: welcome email → intake webhook → AI research memo (every claim carries a source URL or the literal ASSUMPTION: label, human-reviewed) → campaign draft with launch criteria evaluated → human Approve & Launch (Rung 1) → Meta campaign created PAUSED then activated (or a manual spec-sheet fallback). Leadgen webhooks fetch the lead, record consent with the form disclosure as evidence, and the speed-to-lead engine fires the first touch immediately (target <60s, recorded as seconds_to_first_touch). Bookings attribute to lead submissions; no-shows get exactly one recovery email. Reports render from pack definitions: daily ops auto-sends internally; weekly/monthly client reports draft into the human queue.

v1.1 — operator workflow & knowledge layer. Pack onboarding now includes human_task steps (create the client's Meta ad account, attach billing — card data NEVER enters this system) that park the machine until an operator checks them off in the client view, with internal notify_human_queue chasers when a manual step is stuck. Campaign drafts are editable before approval (budget, placements, ad-variant count, formats — allowed sets come from the pack creative_spec); the AI brief regenerates with exactly N numbered variants and every launch criterion re-evaluates against the edited config (a budget above the plan cap is allowed but shows ❌). A client knowledge layer — uploaded documents with best-effort text extraction, plus per-scope/per-channel message guidelines with few-shot examples, human-approved before any AI use — feeds drafted replies, research memos, and campaign briefs. The /setup screen shows red/green status per default provider template with the exact next action (+ live "Test" buttons), imports contact lists from CSV/XLSX/link-readable Google Sheets through the same deduper as the CLI, runs the consent-attestation workflow that upgrades segments to granted (evidence stored as an uploaded document), and imports existing suppression lists. Operational gaps are closed: hosted token-gated intake form (/intake/<token>) and client upload link (/c/<token>/uploads) — the only two unauthenticated writes, both rate-limited; daily auto-enrollment per pack auto_enroll plus an "Enroll audience now" button; a re-drive ticker for quiet-hours/cap-deferred one-off sends (gives up loudly after 5 attempts); consumer quiet-hours timezone defaults per client; inline linking of manually-built Meta campaigns so insights sync picks them up; and immediate internal alerts (warmup halt, repeated job failures, Gmail watch expiry), singleton-keyed to one email per type per hour.

v1.2 — agency settings wizard (pack compiler). Non-technical operators never touch YAML: the /setup "Agency settings" wizard asks plain questions (plan budgets, ad-variant count, allowed/default placements, formats, targeting radius, manual-step checklists, follow-up timing, quiet hours), shows a human-readable diff, and compiles the answers into a NEW pack version through the exact same zod + semantic validation and atomic activation as a hand-authored pack — versioned, auditable (pack.compiled_from_settings carries who/what), and rollback-safe. The wizard reaches tunables only; structure (funnel, step graph, sequences) and safety policy (send caps — SMS stays 0, warmup, classification threshold) are not editable from any UI.

v1.4 — credentials vault. Provider-template setup is fully in-product: the operator pastes each service's API key/secret directly on its /setup row (AES-256-GCM encrypted at rest under env-only CREDENTIALS_SECRET, write-only — values are never echoed back), hits Save, and both processes pick it up without a restart (immediate self-apply + worker /internal/reload-credentials

  • minute-tick hydration; clears reconcile everywhere too). Secrets WE define (Cal.com/Meta/Pub/Sub verify tokens) get a Generate button — minted, stored, and shown once for pasting into the other service's dashboard. The Gmail OAuth callback stores its refresh token itself ("click Connect" is the whole flow). A code-side whitelist keeps infra secrets (DATABASE_URL, auth/signing keys, the master key) env-only; rows needing one-time platform provisioning (Google OAuth app, Pub/Sub, the Meta app + review) are badged "deployment".

v2 — MCP platform (kernel/edge split). The deterministic kernel is untouched; the integration edge is re-platformed onto MCP. The worker now exposes the whole operator surface as an MCP server (19 tools, read + kernel-gated writes; draft_reply_send requires an existing draft AND confirm: true from a human — there is no author-and-send tool). Engines resolve their effectors through a capability registry (operator binding → pack requires default → error): deliverers.ts is a shim over email.send, Meta lead fetch / insights / campaign create go through leads.fetch / ads.insights / ads.campaign, and the builtin servers are real in-process MCP servers, so the protocol path runs on every send. The /setup Connections panel binds/tests/swaps servers (incl. a curated template directory and OAuth 2.1 remotes with vault-backed secrets). The AI provider layer is templated too: Anthropic native plus OpenAI-compatible providers (OpenAI, Ollama/self-hosted, Kimi/Moonshot, OpenRouter, and LiteLLM-style gateways) route through the same aiText / aiJson / aiAgentTurn abstraction, with provider-neutral builtin/ai MCP capabilities reserved for packs that declare AI needs explicitly. The /ask agent console runs a model loop over the same toolset — read/draft by default, approval-bearing tools structurally unreachable, every call audited with the operator's identity. Sources without suitable signed webhooks join via polled *.events capabilities feeding the same idempotent ingestion paths. Multi-tenancy: org_id + row-level security on agency-scoped tables, per-org packs/vault/bindings (withOrg / one instance per org via ORG_ID). The autonomy ladder (Rung 2) promotes an action class to auto-execution only after the pack-declared streak of consecutive clean human approvals and demotes it on a single human edit — always audited, firewall unchanged.

Two senders, never mixed: Gmail for 1:1 replies (threading looks human), Resend on the warmed subdomain for everything bulk — with visible unsubscribe links and RFC 8058 one-click List-Unsubscribe headers on all marketing mail (assembled in the deliverer shim, so rebinding email.send can never strip them). Unsubscribe processing is immediate and suppression is forever.

Webhooks (all signature-verified + idempotent)

Endpoint Source Verification
/webhooks/gmail Google Pub/Sub push verification token
/webhooks/resend Resend (svix) HMAC base64
/webhooks/calcom Cal.com x-cal-signature-256 HMAC
/webhooks/purchase Stripe (assumed, OQ1) stripe-signature t/v1
/webhooks/meta Meta leadgen X-Hub-Signature-256 + GET challenge
/webhooks/intake intake form shared secret header
/unsubscribe one-click unsub HMAC token

Native webhooks are not a contradiction of the MCP-first architecture: they are the trusted event transport for providers that offer signed, low-latency callbacks. MCP event capabilities are the generic fallback for the long tail.

Guardrails enforced in code

Single firewall send path · consent unknown/revoked = blocked · SMS cap 0 until provenance documented · quiet hours per recipient timezone (marketing only) · warmup ramp with bounce/complaint auto-halt (governs marketing on the warmed bulk domain; transactional mail — incl. the halt alert itself — stays under the daily cap) · suppression forever · low-confidence classifications to human review · all client-facing AI text human-reviewed before first use · audit_events append-only (DB trigger) · no vertical strings in application code (tested) · v1.1: unapproved guidelines/examples never reach an AI prompt · card data never enters the system (human_task checklist routes it to Ads Manager) · uploads served only via authenticated routes; client links are per-client HMAC · STL touch copy stays pack-authored · the intake form and upload link are the only unauthenticated writes — token-gated, size-capped, rate-limited · v2: sendThroughFirewall is still the only delivery path — no MCP tool exposes raw delivery, and email.send is callable only from inside the firewall's deliverer (tested) · every MCP/agent tool call audited with actor + tool, payloads logged without message bodies or credentials · bindings change HOW an effect happens, never WHETHER the firewall ran · the agent console is read/draft-only; human approvals are clicked in the UI, never synthesized by the model · vendor API hosts appear only inside the builtin MCP servers (grep-tested) · a down bound server degrades to audited failed/queued sends, never silent loss.

Open questions (spec §14 — answer before P3/P5)

  1. Payment processor + webhook access (Stripe assumed).
  2. Written consent attestation for any imported contact list — gates all P3 sends.
  3. Plan tiers ↔ ad budgets (placeholders in the pack).
  4. Whose Meta Business Manager owns client ad accounts.

Also pending: Meta app review (leads_retrieval, ads_read, pages_show_list + business verification) — submit day one; gates P6 only.

Platform roadmap

These are intentionally not blockers for the lash-agency MVP:

  1. Full SaaS org switching. The code already supports per-org packs, vaults, bindings, and RLS for one runtime instance per org. A shared SaaS control plane still needs auth user → org mapping, org switching in the UI, and per-request org selection.
  2. Pack authoring assistant. The current settings wizard edits safe tunables. The next platform leap is an assistant that interviews an operator, inspects connected systems, drafts a RelayPack, validates it, and opens it for human review before activation.
  3. Connector directory depth. The shipped directory proves the template model. Future work is adding well-tested provider templates for CRMs, calendars, payment processors, chat surfaces, forms, and reporting sources.

About

Vertical operations engine where vertical behavior is data (RelayPacks) and integrations are data (MCP capability bindings). Deterministic kernel — funnel, sequences, action firewall, append-only audit — that is both an MCP server and an MCP client.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages