SaaS · billing and CRM
Stripe Billing to CRM sync: subscriptions, payments and MRR as deals, with no lost or duplicate events
A marketplace app that mirrors Stripe customers, subscriptions and payments into a CRM as contacts and deals, built on a signed-webhook ledger, per-tenant row-level security and a daily reconciliation.
- Industry
- SaaS · billing and CRM
- Technologies
- TypeScript · NestJS · PostgreSQL 16 + Drizzle
Challenge
Companies that bill through Stripe wanted their CRM to show who pays, on which plan and at what monthly value, without manual updates. Stripe webhooks do not arrive in order, can arrive twice and carry a snapshot that may already be out of date, so a sync that trusts them writes wrong stages and duplicate deals. The app also serves many companies at once and holds their Stripe keys, so isolation and secret handling were part of the brief from day one.
Solution
We built a hosted backend that runs three processes from one image: API, worker and scheduler. Ingress verifies the Stripe signature on the raw body, identifies the tenant only from the endpoint token and returns 2xx without running business logic. Each event goes into a ledger and onto a queue in one transaction, with an outbox so nothing is lost if Redis goes down. Before any write, the worker fetches the current object from Stripe and drops stale events, then maps subscriptions to deals with stage, MRR, currency and contact. A daily reconciliation catches anything the webhooks missed.
How it connects
- 01 Verify webhook
- 02 Ledger & queue
- 03 Fetch current state
- 04 Map to deal
- 05 Write to CRM
- 06 Reconcile daily
What we delivered
- Webhook ingress with a raw-body signature check and tenant resolution by endpoint token
- Event ledger with transactional enqueue and an outbox drainer
- Queues with a retry policy, a circuit breaker and per-object locks
- Subscription-to-deal mapping with stage, MRR (flagged when incomplete), currency and contacts
- Deal lookup that verifies its own match before writing
- Initial history import in resumable chunks, plus a daily reconciliation
- Four-step setup wizard in 11 languages that creates the CRM fields and pipeline stages itself
- Several Stripe accounts per user, data retention jobs and installation statistics
- About 590 automated test cases
Functionality
- PostgreSQL row-level security on every business table; unique keys include the tenant and the Stripe account
- Envelope encryption for stored secrets
- Fields and stages that people set by hand in the CRM are never overwritten
- Errors that cannot succeed on retry go to a 'needs action' state instead of retrying forever
- The setup page loads no third-party scripts, because users paste a Stripe key into it
- Daily backups, checked with test restores
- Structured logs with no personal data
Integrations
- Stripe Billing API and webhooks
- The client's CRM API: contacts, deals, pipelines and custom fields
- The client's app marketplace
Technologies
Result
Deployed to a hosted staging server and in acceptance testing against a live Stripe account. Acceptance testing and a security review raised defects, and the ones inside the app are fixed and retested. Marketplace publication follows acceptance.
Why fetch from Stripe before every write?
A webhook tells you that something changed, not what is true now. Two events about the same subscription can arrive in the wrong order, and the older one would overwrite the newer state. So the app reads only the event ID, its type and the object ID from the webhook, then fetches the current object from Stripe before it writes anything to the CRM. The ledger makes a repeated event a no-op, and the daily reconciliation closes any gap left by a missed delivery.
Discuss your project
Tell us about your project