Bulk SMS:
Campaign Engine.
A bulk SMS platform where the campaign composer knows what a message will cost before it is sent, and a retried batch cannot send twice.
Role
Full-stack Developer
Stack
Next.js, TypeScript, PostgreSQL
Timeline
14 Weeks
Scope
Composer, Segments, Delivery
Fig 0.1 – Campaign composer: segment counts, encoding-aware character counter, live delivery states.
01 // The Problem
Sending Blind.
The client sells bulk SMS to insurers and utilities. Their operators built campaigns in a spreadsheet, then handed a CSV to an internal script. Two things went wrong repeatedly. Nobody could see a segment size before the send ran, and one campaign went out to a list that included suppressed numbers. The other problem was quieter and more expensive. SMS bills per 160-character segment, but a single accented character switches the message to a different encoding and drops the limit to 70. Operators counted characters by eye, so messages they believed were one segment were billed as two.
02 // What I Built
Counting
Before Sending.
I built the composer as a single screen. The recipient panel resolves segment counts from the database as you pick them, so the number you see is the number that will send. The character counter detects the encoding from the message body itself, so an accented name in a merge field moves the counter to the 70-character limit before anyone hits send. Suppression is not a step at send time. It is a condition on the query that builds the recipient set, so a suppressed number cannot enter a campaign in the first place. Delivery states stream in from provider webhooks and land on the same screen.
03 // The Hard Part
Retries That
Do Not Duplicate.
Any queue that retries will eventually replay a batch it already sent. In SMS that is not an inconvenience, it is a second message to a real person and a second charge. I keyed every outbound row on the campaign and the recipient number together, with a unique constraint on the pair. A replayed batch collides with rows that already exist and does nothing. That pushes the guarantee into the database instead of into application logic that has to be right every time. It also made the failure reports honest, because a row can only be in one state.
04 // The Audit Trail
Reconstructing
A Send.
The question that arrives after any messaging incident is always the same. Why did this person get this message. Answering it needs more than a delivery log, because the interesting part is what the campaign looked like at the moment it ran, and by the time anyone asks, the segment has been edited and the template has been rewritten. I version the campaign at send time and store the resolved segment definition, the template body and the encoding alongside the batch. A send can be reconstructed months later without trusting that nothing changed underneath it, which is what turns an angry email into a five minute check.
The Build.
What replaced the spreadsheet and the CSV script, counted from the work rather than from a conversion report.