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
/askconsole, a test harness) operates the system through audited tools atPOST :8787/mcpor 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, andgrep -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.)
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)
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 :3000Connect a local MCP client (Claude Desktop / Claude Code) to the running system via stdio:
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 testIntegration tests skip automatically when no test database is reachable.
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.
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.
| 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.
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.
- Payment processor + webhook access (Stripe assumed).
- Written consent attestation for any imported contact list — gates all P3 sends.
- Plan tiers ↔ ad budgets (placeholders in the pack).
- 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.
These are intentionally not blockers for the lash-agency MVP:
- 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.
- 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.
- 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.
{ "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" } } } }