postgres driver. It runs as a single Node/Bun process. There is no message
broker and no separate worker deployment: background work runs inside the app
process, plus a few HTTP endpoints that an external scheduler calls.
The product covers: a lead database (People and Companies), enrichment tables,
email, LinkedIn and WhatsApp outreach, WhatsApp calling, an AI-assisted CRM
with a master inbox, and analytics. Every piece of business data belongs to one
organization (a tenant).
Overview
Directory map
src/lib/<domain>
src/app/api
Route handlers are grouped by domain: auth (Better Auth), grid, leads,
outreach, linkedin, whatsapp, calling, calls, call-recorder
(the extension), crm, analytics, ai, settings, webhooks (Unipile),
plus a few older ones (campaigns, categories, domains, qualify,
targeted-domains, apps, export, nav, dev). Which of these are public
is listed at the top of src/proxy.ts.
Request flow
src/proxy.tsruns first. It lets through a short list of public paths (auth pages and endpoints, Unipile and Gmail webhooks, the call-recorder API, unsubscribe pages, static files). For/api/linkedin/jobsit acceptsAuthorization: Bearer $CRON_SECRET. Everything else needs a Better Auth session cookie; without one, pages redirect to/sign-inand APIs answer 401. This check is optimistic only: it does not validate the session. It also applies the migration kill switches.- The route or page calls
requireOrgContext(request)/withOrgContext(request, fn)(API) orrequirePageOrgContext()(pages) fromsrc/lib/auth/context.ts. This validates the session, reads the active organization, and re-reads the member’s role from the database on every call, so removing someone takes effect immediately. Failures become 401, 403 or 409 JSON. withOrgContextopens anAsyncLocalStoragescope withrunInOrganization(orgId, fn). Code insrc/lib/never takes the session: it reads the organization from the scope (currentOrganizationId()), which is what lets workers and webhooks reuse it. Reading the scope outside one throws.- Queries filter every scoped table with
inOrg(table), and inserts setorganizationId: currentOrganizationId(). An id from another organization is simply not found (404). - Drizzle sends SQL through the one pool in
src/lib/db.ts.
Background work
Workers and webhooks have no session. They resolve the organization from the
data (mailbox, Unipile account, call session, grid job, CRM job) and run each
item inside
runInOrganization. The outreach scheduler has no distributed lock,
so the app must run as a single instance.
Auth and tenancy
Better Auth provides email and password (verified by email), optional Google sign-in, organizations and teams. Roles are owner, admin and member: members use the product; owners and admins also manage integrations, AI settings, members and teams. Better Auth tracks the active organization on the session; scoping data to it is done by this codebase (src/lib/tenancy), not by the library.
Each table is classified in src/lib/tenancy/registry.ts as scoped (has its own
organization_id), inherited (scoped through a NOT NULL parent), global, auth
or legacy, and bun run db:check fails if a table is unclassified. See
multi-tenancy/conventions.md and
multi-tenancy/plan.md.
Integrations store
Platform integrations (Unipile, Google Workspace, R2) are one row each per organization ingrid_providers (key platform-<key>), with the credentials
encrypted in grid_provider_credentials using INTEGRATION_CREDENTIALS_KEY
(AES-256-GCM). Reads are cached 30 seconds per process. OpenRouter and the
enrichment providers use the same tables. Code that calls one of these services
asks src/lib/platform/credentials.ts (getPlatformCredentials,
requirePlatformCredentials, isPlatformConnected) inside the organization
scope, and never reads keys from process.env. See
integrations.md.
Data model overview
84 tables indb/schema.sql. By domain:
people.id is the permanent identity shared by every channel. people and
companies gain user-defined columns at runtime. Details and the rules for
changing the schema are in database.md.
Other parts
- WhatsApp call recorder.
extensions/whatsapp-recorder/is a Chrome extension that dials and records calls inside web.whatsapp.com. It sharessrc/lib/calls/contract.tswith the app and uploads to R2 through presigned URLs, authenticating each call with a bearer token. - Formula sandbox. Table formulas run in a QuickJS isolate
(
src/lib/grid/runners/sandbox.ts) with lodash, moment and formulajs loaded fromnode_modulesat runtime (listed innext.config.tsoutputFileTracingIncludesso the standalone build includes them). - Build output.
next.config.tssetsoutput: "standalone"; the Docker image runsbun server.jsfrom that output.