What you’ll build
A partner integration that:- Receives a Bearer token from the customer during onboarding
- Performs an initial full backfill of all VOMO Users into the external system
- Polls VOMO for ongoing changes and propagates them to the external system
- Reconciles daily to catch gaps and deletions
- Surfaces sync health visibility to the customer
When this recipe fits
| Scenario | This recipe fits |
|---|---|
| Sync VOMO users to an external CRM (Salesforce, HubSpot, etc.) | ✓ |
| Mirror VOMO users into a data warehouse (Snowflake, BigQuery, etc.) | ✓ |
| Feed marketing automation tools with volunteer rosters | ✓ |
| Sync to a customer-built reporting database | ✓ |
| Bidirectional sync (external CRM is source-of-truth for some fields) | ✓ With added coordination — see the bidirectional section |
| Real-time sub-minute sync requirements | ✗ Not feasible without webhooks |
Architecture
Six components, each independent — failures in one don’t crash others:| Component | Purpose |
|---|---|
| Onboarding flow | Captures the customer’s VOMO token; configures the integration |
| Initial backfill | One-time full read of VOMO users at integration start |
| Polling worker | Ongoing incremental sync via updated_after |
| Reconciliation worker | Daily/weekly audit catching gaps |
| Partner state DB | Checkpoints, external-ID mappings, audit log |
| Dead-letter queue | Failed records for later retry |
Step 1: customer onboarding
The onboarding flow handles three things: capture the Bearer token, configure sync targets, trigger the initial backfill.JavaScript
Why initialize the checkpoint to epoch
Setting the checkpoint tonew Date(0) means the next polling cycle would query “everything updated since 1970” — which is everything. But the backfill runs first; once the backfill completes, the checkpoint advances to the latest-seen timestamp. From that point forward, polling operates incrementally.
This approach unifies backfill and steady-state under one mechanism — no separate code paths.
Step 2: initial backfill
The backfill reads all VOMO Users and pushes them to the external system. For a 10,000-user customer, this is ~667 paginated requests at 15 records per page.JavaScript
What processUserForBackfill does
JavaScript
Backfill duration estimates
| Customer size | Backfill duration (at 3 req/sec) |
|---|---|
| 1,000 users | ~3 minutes |
| 10,000 users | ~30 minutes |
| 100,000 users | ~5-6 hours |
| 500,000 users | ~1 day |
Step 3: steady-state polling
After backfill, the polling worker takes over. It runs on a schedule and processes only what’s changed.JavaScript
Why use the mapping table to detect new vs. update
The 200/201 detection works at the VOMO upsert level. Here we’re going the other direction (VOMO → external) — the mapping table is the source of truth for “have we seen this user before in this customer’s integration”:| Mapping exists? | Treatment |
|---|---|
| No | New user from the integration’s perspective; fire user.created |
| Yes | Known user; fire user.updated |
created_at == updated_at). The mapping table is the integration’s persisted view of reality.
Step 4: daily reconciliation
Daily reconciliation catches gaps and re-processes them. See Reconciliation Patterns for the full pattern.JavaScript
Weekly deletion detection
JavaScript
Step 5: customer-facing visibility
Customers ask “is our integration working?” — build a per-customer dashboard that answers in seconds:JavaScript
Bidirectional sync
Some integrations need to also write back to VOMO — e.g., the external CRM is source-of-truth for user phone numbers.JavaScript
Operational concerns
Token expiration
JavaScript
Customer offboarding
JavaScript
Per-customer rate-limit budgets
JavaScript
Things to watch for
A few subtleties that surface in production:The participation gap
This recipe syncs users but not their participations (since participations don’t advanceupdated_at). For integrations that need participation data, see Detecting User Changes: The participation caveat and add a separate participation-polling layer.
Large customers stress the integration
A 100,000-user customer can produce 5,000 changes per day during active periods. The polling worker handles this through pagination, but downstream processing (writes to external system) is often the bottleneck. Monitor per-customer processing rate and scale workers as needed.Backfill scheduling
For very large customers, the initial backfill can take hours. Schedule it for low-traffic hours and communicate timing to the customer. Prevent multiple workers from picking up the same backfill (use a lock or unique-claim pattern).Mapping table growth
Per-customer mapping tables grow unbounded over time. For multi-tenant integrations:- Periodic archival of long-inactive customers’ mapping tables
- Partitioning by customer for query performance
- Indexes on (customer_id, vomo_user_id) and (customer_id, email)
Dead-letter queue management
The DLQ catches failures but isn’t self-cleaning. Build:- Automatic retry of recoverable failures (timeouts, transient 5xx)
- Manual review queue for unrecoverable failures (data validation, permanent destination errors)
- Aging policies (alert on DLQ entries older than N days)
What you’ve built
After this recipe:- ✅ Onboarding flow that captures and validates the Bearer token
- ✅ Initial backfill that mirrors all VOMO users to the external system
- ✅ Polling worker that catches incremental changes every 15-30 minutes
- ✅ Daily reconciliation catching gaps
- ✅ Weekly deletion detection
- ✅ Per-customer dashboards showing sync health
- ✅ Operational handling for token failures, offboarding, and rate budgets
Where to go next
Build a Group from a Query
Build Groups dynamically from query results.
Report on Volunteer Hours
The reporting recipe using participation data.
Reconciliation Patterns
The reconciliation patterns this recipe references.
Sync Architecture Patterns
The broader architectural patterns for sync designs.