← Zpět na blog
19. května 2026·5 min čtení·Tým TopSMS

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 statusOld provider XOld provider YInterní status
queuedpendingsubmittedqueued
sentacceptedin_transitsent
delivereddelivereddelivered_to_phonedelivered
failederrorpermanent_failurefailed
expiredexpiredttl_exceededexpired
rejectedrejecteddenied_by_operatorrejected

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:

  1. Zapnete webhooky pro oba providery (paralelně)
  2. Zpracujete oba formáty
  3. Až po cleanup odstraníte starý handler

Krok 6: Rollback plán

Pokud po jakémkoli kroku zaznamenáte problém:

  1. TOPSMS_TRAFFIC_PERCENT=0 v .env
  2. pm2 restart topsms (5 sekund)
  3. 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:

  1. Otevřete ticket u nového providera s žádostí o tuto featuru
  2. Pokud feature kritická → nemigrujte na nového, zůstaňte u starého
  3. 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ČasKdo
Schválení sender ID u nového providera5 prac. dníProvider
Abstraction layer + factory25 minVývojář
Test na 10 SMS bonus zdarma30 minVývojář
Day 1: 1 % traffic1 min nastaveníVývojář
Day 1-4: monitoring0 min aktivního časuAuto
Day 5: 100 % traffic1 min nastaveníVývojář
Day 14: cleanup30 minVývojář
Celkem aktivního času~1 hodinaVývojář

Co dál

apimigracedeveloperstrategie
Otestujte TopSMS zdarma

10 SMS na start, bez závazku, registrace minuta.

Registrovat zdarma →