<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 정진경</title>
    <description>The latest articles on DEV Community by 정진경 (@_6c011e955e0e518bd4bd6).</description>
    <link>https://dev.to/_6c011e955e0e518bd4bd6</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3819087%2F556ac374-2551-4e30-8008-843bf5eb4d1d.png</url>
      <title>DEV Community: 정진경</title>
      <link>https://dev.to/_6c011e955e0e518bd4bd6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_6c011e955e0e518bd4bd6"/>
    <language>en</language>
    <item>
      <title>I spent 6 months building my own SMS platform from scratch — here's what I learned</title>
      <dc:creator>정진경</dc:creator>
      <pubDate>Wed, 11 Mar 2026 22:00:11 +0000</pubDate>
      <link>https://dev.to/_6c011e955e0e518bd4bd6/i-spent-6-months-building-my-own-sms-platform-from-scratch-heres-what-i-learned-5d1o</link>
      <guid>https://dev.to/_6c011e955e0e518bd4bd6/i-spent-6-months-building-my-own-sms-platform-from-scratch-heres-what-i-learned-5d1o</guid>
      <description>&lt;p&gt;Hey DEV community! 👋&lt;br&gt;
I want to share my journey building a global SMS platform and the technical challenges I faced along the way.&lt;br&gt;
Why I built it&lt;br&gt;
Like many developers, I needed SMS verification codes for my product. The options were painful:&lt;/p&gt;

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

&lt;p&gt;So I decided to build my own. 6 months later, here we are.&lt;/p&gt;

&lt;p&gt;Tech Stack&lt;/p&gt;

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

&lt;p&gt;Challenge 1: SQLite Concurrency Locks&lt;br&gt;
High concurrency kept throwing SQLITE_BUSY: database is locked.&lt;br&gt;
Solution — three steps:&lt;br&gt;
javascript// Step 1: Enable WAL mode&lt;br&gt;
await prisma.$executeRaw&lt;code&gt;PRAGMA journal_mode=WAL;&lt;/code&gt;&lt;br&gt;
await prisma.$executeRaw&lt;code&gt;PRAGMA synchronous=NORMAL;&lt;/code&gt;&lt;br&gt;
await prisma.$executeRaw&lt;code&gt;PRAGMA busy_timeout=5000;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;// Step 2: Single global Prisma instance&lt;br&gt;
if (!global.&lt;strong&gt;prisma) {&lt;br&gt;
  global.&lt;/strong&gt;prisma = new PrismaClient();&lt;br&gt;
}&lt;br&gt;
export default global.__prisma;&lt;/p&gt;

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

&lt;p&gt;async add(fn) {&lt;br&gt;
    return new Promise((resolve, reject) =&amp;gt; {&lt;br&gt;
      this.queue.push({ fn, resolve, reject });&lt;br&gt;
      this.run();&lt;br&gt;
    });&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;async run() {&lt;br&gt;
    if (this.running) return;&lt;br&gt;
    this.running = true;&lt;br&gt;
    while (this.queue.length &amp;gt; 0) {&lt;br&gt;
      const { fn, resolve, reject } = this.queue.shift();&lt;br&gt;
      try { resolve(await fn()); }&lt;br&gt;
      catch (err) { reject(err); }&lt;br&gt;
    }&lt;br&gt;
    this.running = false;&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Challenge 2: Floating Point Precision&lt;br&gt;
SMS billing involves lots of decimal calculations. Users started seeing balances like -0.0000001.&lt;br&gt;
Solution — store everything as integers:&lt;br&gt;
javascript// Store as integer (multiply by 10000)&lt;br&gt;
const toStorage = (amount) =&amp;gt; Math.round(amount * 10000);&lt;/p&gt;

&lt;p&gt;// Read back as decimal&lt;br&gt;
const fromStorage = (stored) =&amp;gt; stored / 10000;&lt;/p&gt;

&lt;p&gt;// All calculations use integers&lt;br&gt;
const deduct = (balance, price) =&amp;gt; {&lt;br&gt;
  return fromStorage(toStorage(balance) - toStorage(price));&lt;br&gt;
};&lt;/p&gt;

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

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

&lt;p&gt;class CarrierAdapter extends BaseAdapter {&lt;br&gt;
  normalize(response) {&lt;br&gt;
    return {&lt;br&gt;
      success: response.status === 'success',&lt;br&gt;
      messageId: response.msgid,&lt;br&gt;
      errorCode: response.error || null&lt;br&gt;
    };&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
Adding a new carrier only requires writing one adapter — core logic stays untouched.&lt;/p&gt;

&lt;p&gt;Challenge 4: Smart Routing&lt;br&gt;
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.&lt;br&gt;
javascriptclass SMSRouter {&lt;br&gt;
  selectBestAdapter() {&lt;br&gt;
    return this.adapters.reduce((best, current) =&amp;gt; {&lt;br&gt;
      return current.score &amp;gt; best.score ? current : best;&lt;br&gt;
    });&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;async send(phone, content) {&lt;br&gt;
    const adapter = this.selectBestAdapter();&lt;br&gt;
    return adapter.send(phone, content);&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Results&lt;br&gt;
The platform now supports:&lt;/p&gt;

&lt;p&gt;🌍 190+ countries worldwide&lt;br&gt;
📡 1,200+ carrier connections&lt;br&gt;
⚡ Average 5-second delivery for domestic SMS&lt;br&gt;
💰 60% cheaper than Twilio for equivalent usage&lt;/p&gt;

&lt;p&gt;What's next&lt;/p&gt;

&lt;p&gt;Adding more payment methods&lt;br&gt;
Building a blog with SMS integration tutorials&lt;br&gt;
Improving webhook reliability&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
Would love to hear if anyone else has built something similar or has tips for scaling SMS infrastructure! 🚀&lt;/p&gt;

&lt;p&gt;Tags: node javascript webdev showdev&lt;/p&gt;

</description>
      <category>backend</category>
      <category>node</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
