Migrace SMS provideru: jak přepnout poskytovatele za hodinu
Praktický postup migrace SMS brány v produkční aplikaci. Postupný traffic switch, zachování idempotence, testovací plán a rollback. Pro vývojáře a tech leady.
TL;DR: Migrace SMS provideru v produkční aplikaci by neměla trvat víc než hodinu aktivního času vývojáře a žádný downtime. Klíč je abstraction layer (interface s 2 implementacemi), postupný traffic switch (1 % → 10 % → 50 % → 100 %), a dual-write fáze kde oba providery běží paralelně. Tento průvodce popisuje konkrétní postup.
Pokud váš e-shop, banka nebo SaaS posílá SMS přes jednoho provideru a chcete přejít na jiného, migrace by neměla být big-bang. Tento článek popisuje produkční postup, který minimalizuje risk a umožňuje rollback do 5 minut.
Před začátkem — checklist
- Otestovaný nový provider (10 SMS bonus, send-test na vlastní telefon)
- API klíče pro nového providera v
.env(ne hardcodované) - Vlastní sender ID schválené u nového providera (~5 prac. dní — začněte předem)
- Mapování statusů (každý provider má vlastní názvy:
delivered,delivery_pending, atd.) - Backup současné kód base / rollback plán
- Mid-migration monitoring (Sentry / Grafana / Datadog)
Krok 1: Abstraction layer (15 minut)
Místo přímého volání API od stávajícího providera vytvořte interface:
// lib/sms-provider.ts
export interface SmsProvider {
send(opts: SendOptions): Promise<SendResult>
getStatus(id: string): Promise<StatusResult>
}
export type SendOptions = {
to: string
from: string
text: string
externalId?: string
}
export type SendResult = {
ok: boolean
messageId?: string
externalId?: string
cost?: number
error?: string
}
export type StatusResult = {
status: 'queued' | 'sent' | 'delivered' | 'failed' | 'expired' | 'rejected'
deliveredAt?: string
operatorCost?: number
carrier?: string
errorMessage?: string
}
Pak vytvořte 2 implementace — pro starého i nového providera:
// lib/sms-provider-old.ts
import { SmsProvider, SendOptions, SendResult } from './sms-provider'
export class OldProvider implements SmsProvider {
async send(opts: SendOptions): Promise<SendResult> {
// ... existing implementation
}
async getStatus(id: string): Promise<StatusResult> { /* ... */ }
}
// lib/sms-provider-topsms.ts
export class TopSmsProvider implements SmsProvider {
async send(opts: SendOptions): Promise<SendResult> {
const res = await fetch('https://www.topsms.cz/api/sms/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.TOPSMS_CLIENT_ID}:${process.env.TOPSMS_SECRET}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: opts.to,
from: opts.from,
text: opts.text,
externalId: opts.externalId,
}),
})
const data = await res.json()
return {
ok: data.ok,
messageId: data.messageId,
externalId: data.externalId,
cost: data.cost,
error: data.error,
}
}
async getStatus(id: string): Promise<StatusResult> { /* ... */ }
}
Krok 2: Factory s % switch (10 minut)
// lib/sms-provider-factory.ts
import { OldProvider } from './sms-provider-old'
import { TopSmsProvider } from './sms-provider-topsms'
const oldProvider = new OldProvider()
const topsmsProvider = new TopSmsProvider()
function topsmsPercent(): number {
return parseInt(process.env.TOPSMS_TRAFFIC_PERCENT || '0', 10)
}
export function getProvider(): SmsProvider {
const pct = topsmsPercent()
if (pct === 0) return oldProvider
if (pct === 100) return topsmsProvider
// Random switch
return Math.random() * 100 < pct ? topsmsProvider : oldProvider
}
V .env nastavte:
TOPSMS_TRAFFIC_PERCENT=0 # start
Pak v kódu místo oldProvider.send(...) voláte getProvider().send(...).
Krok 3: Postupný switch (15 minut + monitoring)
Den 1: 1 %
# .env
TOPSMS_TRAFFIC_PERCENT=1
Restart serveru. Cca 1 % SMS jde přes TopSMS. Sledujte:
- Latence (musí být podobná staré: 2-4 s)
- Doručitelnost (musí být ≥ 99 %)
- Failed rate (≤ 1 %)
- Žádné chybové logy v Sentry/Datadog
Pokud po 24 h vše OK → další krok.
Den 2: 10 %
TOPSMS_TRAFFIC_PERCENT=10
Sledujte 24 h. 10 % traffic je dost na statisticky relevantní porovnání.
Den 3: 50 %
TOPSMS_TRAFFIC_PERCENT=50
A/B test. Pokud nový provider má lepší / horší KPI, uvidíte:
- Stejná doručitelnost? ✓
- Stejná latence? ✓
- Stejná cena za skutečnou konverzi? ✓
- CTR z SMS (přes click tracking) stejný nebo lepší? ✓
Den 5: 100 %
TOPSMS_TRAFFIC_PERCENT=100
Veškerý traffic na TopSMS. Starý provider zůstává jako fallback v kódu, ale nevolán.
Den 14: Cleanup
Po 2 týdnech stabilního provozu odstraňte OldProvider implementaci a factory zjednodušte:
// lib/sms-provider-factory.ts (after cleanup)
export function getProvider(): SmsProvider {
return new TopSmsProvider()
}
Krok 4: Mapování statusů
Každý provider má vlastní pojmenování stavů. Při migraci uchovejte jednotný interní enum:
| TopSMS status | Old provider X | Old provider Y | Interní status |
|---|---|---|---|
queued | pending | submitted | queued |
sent | accepted | in_transit | sent |
delivered | delivered | delivered_to_phone | delivered |
failed | error | permanent_failure | failed |
expired | expired | ttl_exceeded | expired |
rejected | rejected | denied_by_operator | rejected |
V getStatus() mapujte na interní status.
Krok 5: Webhook signature
Pokud používáte delivery webhooky, každý provider má vlastní formát:
TopSMS webhook payload
{
"externalId": "msg-12345",
"to": "+420...",
"status": "delivered",
"deliveredAt": "2026-05-19T10:23:45Z",
"operatorCost": 0.028,
"carrier": "230.2"
}
Validation (HMAC podpis)
import { createHmac } from 'crypto'
function verifyWebhook(body: string, signature: string): boolean {
const secret = process.env.TOPSMS_WEBHOOK_SECRET!
const expected = createHmac('sha256', secret).update(body).digest('hex')
return signature === expected
}
Při migraci tedy:
- Zapnete webhooky pro oba providery (paralelně)
- Zpracujete oba formáty
- Až po cleanup odstraníte starý handler
Krok 6: Rollback plán
Pokud po jakémkoli kroku zaznamenáte problém:
TOPSMS_TRAFFIC_PERCENT=0v.envpm2 restart topsms(5 sekund)- Veškerý traffic zpět na starého providera
Žádný redeploy, žádný git revert — jen jedna env proměnná. To je síla této architektury.
Migrace databáze (volitelné)
Pokud máte v DB záznamy se starým provider_id (např. gosms_id), zachovejte je. Nový provider má vlastní topsms_id. Schema:
ALTER TABLE messages ADD COLUMN topsms_id VARCHAR(40) NULL;
CREATE INDEX idx_messages_topsms_id ON messages(topsms_id);
-- Old provider_id zůstává pro historický audit
Co když má starý provider funkci, kterou nový nemá?
Praktický příklad: starý provider má feature, kterou nový nemá (např. specifický sender ID format). Postup:
- Otevřete ticket u nového providera s žádostí o tuto featuru
- Pokud feature kritická → nemigrujte na nového, zůstaňte u starého
- Pokud feature nice-to-have → migrujte, akceptujte degradaci v té oblasti
Před migrací udělejte gap analysis:
- Sender ID — všichni provideři podporují
- REST API — všichni podporují
- SMPP — TopSMS podporuje, ne všichni provideři ano
- Zkracovač linků + click analytika — TopSMS v ceně, jinde placený add-on
- Webhook delivery reports — TopSMS podporuje, ostatní obvykle taky
Časové okno na migraci
| Aktivita | Čas | Kdo |
|---|---|---|
| Schválení sender ID u nového providera | 5 prac. dní | Provider |
| Abstraction layer + factory | 25 min | Vývojář |
| Test na 10 SMS bonus zdarma | 30 min | Vývojář |
| Day 1: 1 % traffic | 1 min nastavení | Vývojář |
| Day 1-4: monitoring | 0 min aktivního času | Auto |
| Day 5: 100 % traffic | 1 min nastavení | Vývojář |
| Day 14: cleanup | 30 min | Vývojář |
| Celkem aktivního času | ~1 hodina | Vývojář |