Architecture
A managed cold-email deliverability engine — outbound email on your customer-supplied leads — running on phi-cloud for inference, pooled sender mailboxes over SMTP for email delivery, Supabase/Postgres for multi-tenant data, and Stripe for billing.
On this page
CogniLead is a managed cold-email deliverability engine. You supply the leads (via the dashboard or POST /api/v1/leads) and CogniLead runs the hard part: warmed sender-domain pools, a reputation circuit-breaker, personalised multi-step email sequences, reply and bounce handling, suppression, and one-click unsubscribe. The same engine that grows the portfolio is the product CogniLead sells — multi-tenant and Stripe-billed.
What ships today
- Outbound email deliverability — the core. Personalised, multi-step email sequences sent over SMTP from mailboxes in a jurisdiction-aware warmed sender pool, with a Postmaster-Tools-driven reputation circuit-breaker, IMAP reply correlation, bounce handling, suppression, and RFC 8058 one-click unsubscribe.
- Opt-in lead enrichment (paid add-on). POST /api/v1/leads with enrich: true buys the "why now" fact from an enrichment vendor before personalising — billed per enriched lead, and only when the vendor returns a citable, sourced fact.
- Lead intake by API. Leads enter the system via POST /api/v1/leads (customer-supplied), and pass through an intersect gate (verification, suppression, dedup) before anything is scheduled to send. A separate, admin-triggered MarketPrior sourcing pass can also populate leads for a tenant, gated by a per-jurisdiction legal-basis table — see §3.
Infrastructure
- phi-cloud — LLM provider for outbound personalisation, reached through lib/pipeline/adapters/phi-gateway.ts. Routing is jurisdiction-aware (a CH recipient gets CH-resident inference, an EU recipient gets EU-resident inference, and so on).
- SMTP, per sender mailbox — the outbound transport. Every send goes out over SMTP via nodemailer (lib/pipeline/adapters/resend-smtp.ts). A pool mailbox can carry its own transport (lib/pipeline/adapters/mailbox-transport.ts: a contracted mailbox provider, or mail servers CogniLead operates); a mailbox without one, and the application's own notification email, uses the process-wide SMTP relay configured by SMTP_HOST/SMTP_USER/SMTP_PASS (Amazon SES in production). Account emails (sign-up confirmation, sign-in links, password recovery) are sent by the authentication provider through Resend, not through this relay. The "resend" naming on these files is historical and predates a full migration off the Resend vendor. Non-production environments with no SMTP relay configured fall back to an in-memory stub (createMemoryResend()) that records every dispatch without sending real mail.
- Stripe — billing (Checkout, Customer Portal, subscriptions, metered overage) via lib/pipeline/adapters/stripe.ts and the plan catalog in lib/pipeline/billing/plans.ts.
- Supabase / Postgres — the production data layer with row-level security. A local SQLite file or an in-memory repository stand in for it in dev and tests (see "Data layer & tenancy" below).
Tenancy model
Every read and write in the pipeline runs inside a tenant frame. lib/pipeline/tenant.ts keeps the current tenant id in a Node AsyncLocalStorage (ALS) instance pinned to globalThis (so multiple bundled copies of the module still share one ALS across a Next.js worker). Route handlers enter that frame via withTenant(tenantId, …) — typically as the last step of requireAuth() resolution — and every repo call downstream reads the ALS frame rather than trusting a caller-supplied tenant id. A request with no active frame falls back to the sentinel tenant 'cognilead:internal', which should never happen on a real HTTP request (it exists for scripts and fixtures).
A second, narrower frame — withCustomer() — nests inside the tenant frame for reselling tenants: a customer-scoped API sub-key (api_keys.customer_id set) opens this frame so Postgres RLS (current_customer_id(), migration 0032) and the equivalent SQLite/memory-repo filters narrow every row to one end-customer of a tenant that resells CogniLead access. A tenant-level session or key opens no customer frame and sees the aggregate across all of its customers.
Auth model
Two credential shapes are accepted at the HTTP boundary (lib/auth/requireAuth.ts): an Authorization: Bearer pk_live_… server-side API key, or a Supabase SSR session cookie from the dashboard. API keys are looked up by an 8-character prefix under a sentinel tenant frame (so the lookup itself doesn't need to already know the real tenant), then argon2id-verified; a session is resolved through Supabase's getUser() revalidation (not a local JWT decode) so a server-side revocation is honoured immediately, and the caller's tenant + role come from their tenant_members row.
Every route declares a minimum role via withRole(): owner (rank 4) > admin (3) > member (2) > viewer (1), defined in lib/auth/roles.ts. A caller below the minimum gets a 403 with { error: "forbidden", required, actual }; a missing/invalid credential gets a 401. A small family of operator-only routes under /api/v1/internal/pool/* additionally require withSessionOnly — they reject API-key auth outright (even an admin-role key), because their only legitimate callers are the dashboard's own session and the in-process warming/replenish cron, neither of which ever presents a bearer key.
Data layer
lib/pipeline/db/dialect.ts picks the storage backend at process start: DATABASE_URL set → Postgres (Supabase in production); else SQLITE_PATH set, or NODE_ENV unset in dev, → a local SQLite file (default ./data/cognilead.db); else NODE_ENV === "test" → a pure in-memory repository that never touches disk. All three dialects implement the same Repository interface, so pipeline code never branches on which one is active. The schema evolves through 48+ paired migration files (lib/pipeline/db/migrations/000N-*.pg.sql and *.sqlite.sql) covering tenants, leads, sends, sender domains, api keys, suppressions, webhooks, customers, and the durable job queue, with Postgres row-level security policies layered on top (schema.rls.sql) that gate every table on current_tenant_id() / current_customer_id().
Billing
Plans are a single source of truth in lib/pipeline/billing/plans.ts: Free (100 sends/mo hard cap, $0), Starter (6,000 sends/mo, $79/mo + $0.045/send overage), Growth (50,000 sends/mo, $399/mo + $0.035/send overage), Scale (150,000 sends/mo, $1,499/mo + $0.028/send overage), and a custom Enterprise tier. A tenant with no subscriptions row defaults to Free rather than being blocked outright. lib/pipeline/billing/quota.ts enforces the cap immediately before every send is handed to its SMTP transport: on the free tier (or any tier with a zero overage rate) a send at or past the cap is hard-skipped with skipped_reason="quota_exceeded"; on a paid tier past its cap the send still goes out and is metered as billable overage instead.