DEV Community

μ •μ§„κ²½
μ •μ§„κ²½

Posted on

I spent 6 months building my own SMS platform from scratch β€” here's what I learned

Hey DEV community! πŸ‘‹
I want to share my journey building a global SMS platform and the technical challenges I faced along the way.
Why I built it
Like many developers, I needed SMS verification codes for my product. The options were painful:

Twilio: Reliable but expensive, monthly bills hurt
Local providers: Cheap but don't support international numbers
Middlemen: One time a campaign SMS was delayed 20 minutes, users complained like crazy

So I decided to build my own. 6 months later, here we are.

Tech Stack

Backend: Node.js + Express + Prisma
Database: SQLite (with WAL mode for concurrency)
Frontend: React 18 + Vite + Tailwind CSS
Infrastructure: Cloudflare Tunnel (no server needed!)

Challenge 1: SQLite Concurrency Locks
High concurrency kept throwing SQLITE_BUSY: database is locked.
Solution β€” three steps:
javascript// Step 1: Enable WAL mode
await prisma.$executeRawPRAGMA journal_mode=WAL;
await prisma.$executeRawPRAGMA synchronous=NORMAL;
await prisma.$executeRawPRAGMA busy_timeout=5000;

// Step 2: Single global Prisma instance
if (!global.prisma) {
global.
prisma = new PrismaClient();
}
export default global.__prisma;

// Step 3: Serialize write operations
class SerialQueue {
constructor() {
this.queue = [];
this.running = false;
}

async add(fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
this.run();
});
}

async run() {
if (this.running) return;
this.running = true;
while (this.queue.length > 0) {
const { fn, resolve, reject } = this.queue.shift();
try { resolve(await fn()); }
catch (err) { reject(err); }
}
this.running = false;
}
}

Challenge 2: Floating Point Precision
SMS billing involves lots of decimal calculations. Users started seeing balances like -0.0000001.
Solution β€” store everything as integers:
javascript// Store as integer (multiply by 10000)
const toStorage = (amount) => Math.round(amount * 10000);

// Read back as decimal
const fromStorage = (stored) => stored / 10000;

// All calculations use integers
const deduct = (balance, price) => {
return fromStorage(toStorage(balance) - toStorage(price));
};

Challenge 3: 1200+ Carrier Response Formats
Every carrier returns different formats β€” some XML, some JSON, different field names, different success codes.
Solution β€” Adapter Pattern:
javascriptclass BaseAdapter {
async send(phone, content) {
throw new Error('Not implemented');
}

normalize(response) {
// Each carrier implements this
return {
success: false,
messageId: null,
errorCode: null
};
}
}

class CarrierAdapter extends BaseAdapter {
normalize(response) {
return {
success: response.status === 'success',
messageId: response.msgid,
errorCode: response.error || null
};
}
}
Adding a new carrier only requires writing one adapter β€” core logic stays untouched.

Challenge 4: Smart Routing
Multiple carrier channels running simultaneously. Built a real-time scoring system based on success rate, latency, and cost. Messages automatically route to the best channel, with instant failover if one goes down.
javascriptclass SMSRouter {
selectBestAdapter() {
return this.adapters.reduce((best, current) => {
return current.score > best.score ? current : best;
});
}

async send(phone, content) {
const adapter = this.selectBestAdapter();
return adapter.send(phone, content);
}
}

Results
The platform now supports:

🌍 190+ countries worldwide
πŸ“‘ 1,200+ carrier connections
⚑ Average 5-second delivery for domestic SMS
πŸ’° 60% cheaper than Twilio for equivalent usage

What's next

Adding more payment methods
Building a blog with SMS integration tutorials
Improving webhook reliability

If you're building something that needs SMS functionality, feel free to check it out at pulsemsg.vip β€” there's a free testing feature built in, no credit card required.
Would love to hear if anyone else has built something similar or has tips for scaling SMS infrastructure! πŸš€

Tags: node javascript webdev showdev

Top comments (1)

Collapse
 
bridgexapi profile image
BridgeXAPI

This is a great breakdown.

Building on top of multiple carriers really shows how complex delivery actually is.

One thing I’ve seen in production is that routing decisions end up being the most critical factor, especially for OTP flows.

Most systems solve sending and even smart routing, but don’t expose how traffic is actually routed.

That’s usually where reliability issues become hard to debug.

Curious if you considered exposing routing decisions as part of the API.