ADR-009 — CRM via Twenty (sync connector), not a native build
reference· backend · updated 2026-06-23 · source
- Status: Accepted
- Date: 2026-06-23
- Owner: Backend (José)
- Related issue: #741 (depends on ADR-007 Layer-2 ingestion; supersedes the native leads screen)
Context
- The 9-screen Comprender effort (#727) surfaced that
leads— the only true CRM screen — had no@tedos/shared/apicontract, unlike the other sections. We had to decide build a native CRM vs integrate an OSS CRM. - TED OS is a white-label solution-builder; "absorb an external OSS tool cleanly" is itself a capability worth proving (the integrate thesis), and CRM is broad — a native build would chase a moving target (pipelines, activities, automation, dedup, reporting).
- A platform CRM does not cover Comprender's full domain: Mexican fiscal invoicing (CFDI) and courses/LMS stay ours regardless.
Decision
- Headline: Integrate Twenty (OSS CRM) as the CRM system-of-record via a sync connector;
the native
leadsscreen is dismissed. - Operators do CRM in Twenty's own UI; a tenant connector keeps our Postgres in sync so a CRM contact unifies with their student identities through the existing dedupe path.
- Twenty contacts land on a new person identity channel
crmvia the ADR-007 Layer-2 ingestion seam (ingestRecord/ingestViaConnector) —upsertAndLinkdoes the merge. - Connection = sync (API key + webhooks), chosen after validation (below). Not embed/SSO.
- Scope boundaries: this does NOT white-label Twenty's UI (AGPL + maintenance), does NOT replace
payments/CFDI or courses, and does NOT add
crmto the Resumen'speopleByChannel(that stays acquisition-channel only: live/recorded/posgrado).
Validation findings (Twenty, June 2026)
| Dimension | Finding | Consequence |
|---|---|---|
| License | AGPL-3.0 | Unmodified use (API) is clean; white-labeling its UI would trigger AGPL → avoid. |
| Self-host | Docker + Postgres + Redis, ~$20–50 VPS | Run per client; cheap. |
| Multi-tenant | IS_MULTIWORKSPACE_ENABLED=true + wildcard DNS → one workspace per client | Fits white-label. |
| API | Full REST + GraphQL, API-key auth, webhooks | The integration surface. |
| SSO/Clerk | Google + Microsoft + email/pw; no SAML/OIDC (paid Org tier / Authentik proxy) | No clean Clerk session → embed/SSO rejected. |
| White-label | None documented | An embed would look like Twenty, not us → another reason to sync. |
| CFDI / courses | Pure CRM — neither | We keep payments/CFDI + LMS. |
Consequences
Positive
- Proves the integrate thesis; we maintain ~no CRM UI. The contract/seam is the swap point — any
other OSS CRM (or a future native build) implements the same
IngestRecordflow without UI change. - Lead↔student unification is free (same
person+ dedupe), giving a real conversion view. - AGPL-clean: we call Twenty, never modify it.
Negative / trade-offs
- Operators use a second, Twenty-branded tool (no shared Clerk session today — Twenty has no OIDC).
- Sync is one-way (Twenty → us) for v1; writing back to Twenty is out of scope.
- Twenty's exact webhook signature scheme + REST
peopleshape are confirmed against a live workspace in the ops follow-up (built defensively meanwhile).
Follow-ups required
- Ops (effort #741 sub-issue): self-host a client Twenty workspace, wire per-client API key +
webhook secret + base URL (
credentials.twenty), run a backfill, validate end-to-end live. - Scheduled pg-boss backfill job (uses
fetchTwentyPeoplefrom@tedos/engine). - Per-tenant webhook secrets via the credential store (v1 uses one
TWENTY_WEBHOOK_SECRETenv).
Alternatives considered
| Option | Why not |
|---|---|
| Build a native CRM | Chases a broad moving target; the contact backbone (person) already exists, but pipelines/activities/automation/reporting are weeks of work better bought. |
| Embed Twenty via Clerk SSO | Blocked: Twenty has no native SAML/OIDC (paid tier / Authentik proxy), no UI white-label; the embed wouldn't share our session or look like us. |
| API-behind-our-seam (our UI on Twenty's API) | Contradicts dismissing the native leads UI; couples our UI to Twenty's schema/uptime. |
References
- Effort #741 (parent issue) — the build.
knowledge/decisions/adrs/ADR-007-client-data-memory-assistant.md— the Layer-2 ingestion seam this rides on.- Code:
packages/domain/src/ingest.twenty.ts(translator),packages/engine/src/connectors/twenty.ts(capability +fetchTwentyPeople),apps/api/src/routes/twenty-webhook.ts(route),packages/shared/src/domain/person.ts(crmchannel). - Twenty docs: https://docs.twenty.com/developers/self-host/capabilities/setup · https://docs.twenty.com/developers/api-and-webhooks/api