Relationships
#1665 Sync every signup to Attio, and backfill the existing population (#809 follow-up)
Opened by keeb · 8/15/2026· Shipped 8/15/2026
Follow-up to #809, which shipped the Person link but not the population it was asked for.
#809 asked that "every signup should land in Attio as a product User (linked to a Person), and all existing users should be backfilled." What shipped in #1088 was the missing users.person linkage — real, and it closed a measured gap (person fill rate was 0 across every synced user record) — but only for the collectives the sync already admits.
The gap, measured against prod
| count | |
|---|---|
| Prod operatives | 561 |
| Collectives | 608 |
| …with a trial started | 10 |
| …with a subscription | 11 |
| Operatives inside those (what reaches Attio today) | 22 |
| Operatives with an Attio Person already | 46 |
| Operatives with no CRM presence at all | 515 |
The CRM's contacts and the product's users are today two nearly-disjoint populations: Attio holds 270 distinct addresses, and only 46 of them belong to an operative. Those 270 came from outreach work, not from this sync.
What is missing
- Scope.
isInScope(lib/app/attio-desired-state.ts) admits a collective only ontrialStartedAtor a subscription document. An ordinary signup starts no trial — the personal collective is provisioned byprovision-operative.ts, which never goes throughcreateCollective— so a free operative never reaches Attio at all. - The two publish points #809 named.
enqueueAttioSynchas exactly four callers today:collective-trial.ts,trial-ended-watch.ts,admin/extend-collective-trial.ts,billing/sync-subscription.ts. There is no publish fromdatabaseHooks.user.create.after(signup) and none fromemailVerification.onEmailVerification. - The backfill. No
lib/app/backfill-attio-users.ts, and no marker-guarded enqueue over the existing population. The hourly stage sweep only walks collectives already in scope, so it does not serve as one.
What this would cost
Widening to every operative takes the people object from ~270 addresses to ~785 — 515 creates — and puts the entire free user base into a paid third-party CRM. That is the trade #809 asked for and that the #1088 scope decision deliberately declined; Resend already owns every-account contact sync, and Attio has been treated as a sales CRM. This issue exists to make that a decision someone takes deliberately, rather than an absence nobody notices.
If the answer is "yes, but not all of it", the obvious middle grounds are: operatives with a linked account, operatives with non-zero activity, or Person-only (contact grain) for everyone while keeping the User+Workspace grain commercial.
Three things the implementer must not rediscover
CRM_MAPPING_VERSION(inlib/repositories/crm-records.ts) has to be bumped if the payload shape moves.processAttioSyncshort-circuits on a fingerprint of the desired state, which cannot see a transport-side change; without a bump the work is invisible in production while every test passes. It also does not trigger a re-enqueue on its own — a repaint rides the sweep's 24h staleness window.- A Person is resolved by lookup-then-create, never asserted. Attio's
PUToverwrites a multiselect, andemail_addressesis multi-valued — six real people in the workspace hold a curated personal-plus-work pair that an assert would strip. At full scope this matters more, not less: of the 22 currently synced, 5 already link to an existing Person rather than create one. - Volume. Writes are capped at 25/s and shared with the humans using the workspace, and every write lands in the activity feed. A backfill of 515 needs the same drip discipline the queue already gives; the fingerprint memo keeps the steady state silent afterwards.
Acceptance
- A new signup appears in Attio without a trial or a subscription.
- Email verification flips the verified attribute on the same record.
- The existing population lands via a marker-guarded backfill, not a one-shot script.
- Re-running any of it writes nothing the second time.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.