Skip to main content
Most teams do not arrive from another vendor. They arrive from a sendEmail() helper that grew for three years. This guide maps that code to Courier and moves it across one notification type at a time, with both systems running until the logs agree.

What you already built

A homegrown system is rarely one component. It is a provider SDK, a template directory, a preferences table, and a cron job, each added when something broke. The preferences row is the one teams underestimate. A notification_preferences table is easy to add and hard to finish, because every new notification type needs a column, a migration, and a check at every call site.

What this actually buys you

Frame the project as deleting code you maintain, not as adopting a platform. That is what it is, and it is the honest way to size it. Provider changes stop being deploys. Swapping SendGrid for SES becomes configuration. Adding a backup provider becomes configuration. Copy changes stop being tickets. A PM edits a template and publishes. Your queue does not know the difference. The failure modes get names. A homegrown send that vanishes leaves you grepping logs. Courier gives every message a and a reason, so UNROUTABLE and BOUNCED and OPT_IN_REQUIRED are distinguishable. Cost is worth checking early rather than at the end. is the number to model against, and it counts sends rather than seats or channels.

Migrate incrementally

Nobody rewrites every notification in one release. Move one type, prove it, then move the next.
1

Inventory what you send

List every notification your code can produce, and for each one record the trigger, the channel, and the audience.The list is usually longer than anyone expects, and it is the artefact the rest of the migration runs on. Sort it by volume. The lowest-volume, least-critical notification is the one to move first.
2

Connect the providers you already use

Add your existing SendGrid, Twilio, or FCM credentials as . You keep the same accounts, the same sending domains, and the same sender reputation.Nothing about deliverability changes at this step, which is what makes the cutover reversible.
3

Move your users in

Send your existing user records to and keep the id your database already uses. covers the bulk path.Reusing your own user_id matters more than it looks. It means no mapping table, and it means a send from anywhere in your code can address a user with the id it already has.
4

Rebuild one notification

Take the lowest-stakes item from your inventory. Recreate its content as a , then replace that one call site with a Courier send.Leave every other call site alone.
5

Run both, and compare

Keep your old path sending, and have Courier send to a test recipient in the . Compare what arrives.You are checking three things: the content renders the same, the recipient resolves to the same person, and the timing is what you expect.
6

Cut over, then delete

Point production at the Courier send and remove the old code path. Not commented out, removed. A dead path that still compiles is one someone reintroduces in six months.Then take the next item from the inventory.

Verify

1

Confirm delivery in the logs

Open and find the message. The timeline shows routing, rendering, and the provider handoff.
2

Check a preference is honored

Opt a test user out of the topic, send again, and confirm the message is FILTERED rather than delivered. That proves your preferences moved, which is the part most likely to be missed.
3

Compare against the old system

For the notification you moved, confirm the volume in Courier matches what your old path used to send over the same period. A gap means a call site you have not found yet.

What you can delete at the end

The point of the inventory is that it doubles as a deletion list.
  • Provider SDK wrappers, and the per-provider error handling around them
  • The template directory, and whatever renders it
  • The preferences table, its migrations, and every call-site check
  • Retry, backoff, and dead-letter queues for notification sends
  • The cron jobs that batch digests
  • The WebSocket server behind your in-app feed, if you adopt
Keep one thing: whatever decides that a notification should happen. That logic is your product, and it belongs in your code. Courier owns everything after that decision.