Kody SMS w Supabase z własnego numeru: Send SMS hook

Sprawdzone z dokumentacją:

Włączasz w Supabase Send SMS hook typu HTTPS i wskazujesz Edge Function. Supabase Auth wysyła do niej podpisany JSON z user.phone i sms.otp, funkcja sprawdza podpis biblioteką standardwebhooks, wywołuje POST https://smsportal.app/api/v1/gateway/send-sms z nagłówkiem x-api-key i zwraca {}. Kod przychodzi z Twojego numeru. Wydłuż ważność kodu z domyślnych 60 sekund do 5 minut.

Czego potrzebujesz

  • Projekt Supabase (Send SMS hook jest dostępny także w planie Free) i Supabase CLI.
  • Konto smsportal z podłączonym telefonem z Androidem i kartą SIM oraz klucz API (Klucze API → Wygeneruj klucz API).
  • Mała lub wewnętrzna skala: telefon wysyła około 720 SMS-ów na godzinę, a każdy dochodzi w kilka sekund.

Jak wysyłać kody Supabase SMS-em z własnego numeru

  1. Utwórz Edge Function

    Uruchom supabase functions new send-sms i wklej kod do index.ts. Funkcja sprawdza podpis z nagłówków webhook-id, webhook-timestamp i webhook-signature, wysyła SMS i zwraca pusty JSON. Na wysłanie ma 4 sekundy, bo Supabase czeka na hook najwyżej 5 sekund. Przy błędzie zwraca status 200 z obiektem error: tylko wtedy aplikacja dostanie czytelny komunikat.

    Edge Function send-sms
    // supabase/functions/send-sms/index.ts
    import { Webhook } from 'npm:standardwebhooks@1.0.0'
    
    interface SendSmsEvent {
      user: { phone?: string }
      sms: { otp: string; phone?: string }
    }
    
    const SMSPORTAL_URL = 'https://smsportal.app/api/v1/gateway/send-sms'
    const apiKey = Deno.env.get('SMSPORTAL_API_KEY') ?? ''
    const hookSecret = (Deno.env.get('SEND_SMS_HOOK_SECRET') ?? '').replace('v1,whsec_', '')
    
    const json = (body: unknown, status = 200) =>
      new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } })
    
    // Supabase Auth przekazuje aplikacji komunikat tylko z odpowiedzi 200 z JSON-em.
    const authError = (message: string) => json({ error: { http_code: 500, message } })
    
    Deno.serve(async (req) => {
      const payload = await req.text()
      let event: SendSmsEvent
      try {
        event = new Webhook(hookSecret).verify(payload, Object.fromEntries(req.headers)) as SendSmsEvent
      } catch {
        return json({ error: 'invalid signature' }, 401)
      }
    
      // Supabase zapisuje numery bez "+"; przy zmianie numeru cel jest w sms.phone.
      const raw = event.sms.phone || event.user.phone || ''
      const to = raw.startsWith('+') ? raw : '+' + raw
      const message = 'Twój kod logowania: ' + event.sms.otp + '. Ważny 5 minut. Nie podawaj go nikomu.'
    
      try {
        const res = await fetch(SMSPORTAL_URL, {
          method: 'POST',
          headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
          body: JSON.stringify({ recipients: [to], message }),
          signal: AbortSignal.timeout(4000),
        })
        if (!res.ok) {
          console.error('smsportal', res.status, await res.text())
          return authError('Nie udało się wysłać kodu SMS. Spróbuj za minutę.')
        }
      } catch (err) {
        console.error('smsportal unreachable', err)
        return authError('Nie udało się wysłać kodu SMS. Spróbuj za minutę.')
      }
    
      return json({})
    })
  2. Włącz hook w panelu

    W panelu Supabase otwórz Authentication → Auth Hooks → Add hook → Send SMS hook. Włącz Enable Send SMS hook, Hook type: HTTPS, URL: https://<project-ref>.supabase.co/functions/v1/send-sms. Kliknij Generate secret, skopiuj sekret w formacie v1,whsec_... i zapisz hook.

  3. Ustaw sekrety i wdróż funkcję

    Zapisz klucz smsportal i sekret hooka jako sekrety funkcji, a potem wdróż ją z --no-verify-jwt: Supabase Auth wywołuje hook bez tokenu użytkownika, a autentyczność zapewnia podpis. Funkcja widzi nowe sekrety od razu, bez ponownego wdrożenia.

    Supabase CLI
    supabase secrets set SMSPORTAL_API_KEY=TWOJ_KLUCZ_API
    supabase secrets set SEND_SMS_HOOK_SECRET='v1,whsec_WKLEJ_SEKRET_Z_PANELU'
    supabase functions deploy send-sms --no-verify-jwt
  4. Włącz logowanie telefonem i wydłuż ważność kodu

    W Authentication → Sign In / Providers → Phone włącz Enable Phone provider. Przy włączonym hooku dane dostawcy SMS (Twilio i inni) nie są wymagane. Ustaw SMS OTP Expiry na 300 sekund: domyślne 60 sekund to za mało, gdy SMS idzie przez telefon i może chwilę poczekać w kolejce.

  5. Przetestuj na własnym numerze

    Wywołaj signInWithOtp z numerem w formacie +48600100200, wpisz kod z SMS-a w verifyOtp. Logi funkcji (Edge Functions → send-sms → Logs) pokażą każdą odpowiedź smsportal inną niż 2xx.

    Test w supabase-js
    // logowanie numerem telefonu (supabase-js)
    const { error } = await supabase.auth.signInWithOtp({ phone: '+48600100200' })
    
    // po wpisaniu kodu z SMS-a
    const { data, error: verifyError } = await supabase.auth.verifyOtp({
      phone: '+48600100200',
      token: '123456',
      type: 'sms',
    })

Gotowe scenariusze

  • Logowanie numerem w panelu klienta

    Mały panel dla klientów albo narzędzie wewnętrzne loguje użytkowników kodem SMS. Klient widzi Twój numer, więc wie, kto pisze, i nie dostaje SMS-a od nieznanej nazwy.

  • Potwierdzenie nowego numeru

    Gdy użytkownik zmienia numer przez updateUser({ phone }), Supabase wysyła kod na nowy numer. Funkcja z przepisu bierze go z sms.phone, więc kod nie trafi na stary numer.

  • Alarm, gdy kod nie wyszedł

    W smsportal dodaj webhook na MESSAGE_FAILED i przekaż go e-mailem albo na Slacka. Telefon offline oznacza, że kody czekają w kolejce, a po 5 minutach są już nieważne. Szybka informacja pozwala przełączyć logowanie na e-mail.

Błędy i rozwiązania

Aplikacja dostaje Hook requires authorization token
Przyczyna: Funkcja wdrożona bez --no-verify-jwt albo zły sekret hooka, przez co funkcja odpowiada 401.
Co zrobić: Wdróż ponownie z --no-verify-jwt. Sprawdź, czy SEND_SMS_HOOK_SECRET to ten sam sekret, który pokazuje panel Auth Hooks.
Failed to reach hook within maximum time of 5 seconds
Przyczyna: Funkcja odpowiada zbyt wolno, np. bez limitu czasu na wywołanie API.
Co zrobić: Zostaw AbortSignal.timeout(4000) z przepisu. Wywołanie smsportal tylko kolejkuje SMS, więc zwykle trwa ułamek sekundy.
Kod przychodzi, ale verifyOtp mówi, że wygasł
Przyczyna: Domyślna ważność kodu SMS w Supabase to 60 sekund, a SMS z telefonu dochodzi w kilka sekund lub dłużej.
Co zrobić: Ustaw SMS OTP Expiry na 300 sekund. Dłużej niż 10 minut nie warto: NIST każe traktować taki kod jako nieważny.
Błąd 429 over_sms_send_rate_limit
Przyczyna: Limit samego Supabase: domyślnie 30 SMS-ów na godzinę na projekt i jeden kod na numer co 60 sekund.
Co zrobić: Podnieś limit w Authentication → Rate Limits. Limit 60 sekund na numer zostaw, chroni przed zalewaniem kogoś kodami.
Logi funkcji pokazują smsportal 400 albo 429
Przyczyna: 400: brak włączonego telefonu albo niezweryfikowany e-mail konta. 429: wyczerpany limit SMS-ów w pakiecie.
Co zrobić: Sprawdź stan telefonu w panelu smsportal albo zmień pakiet w Cenniku.

Najczęstsze pytania

Czy Supabase nadal wysyła SMS przez Twilio, gdy hook jest włączony?

Nie. Gdy Send SMS hook jest włączony, Supabase Auth nie wywołuje wbudowanego dostawcy, tylko Twoją funkcję. Kod generuje i sprawdza nadal Supabase, Ty odpowiadasz tylko za dostarczenie.

Czy to działa też dla MFA przez telefon?

Tak, MFA przez telefon używa tej samej konfiguracji i tego samego hooka. W Supabase to płatny dodatek (Advanced MFA Phone) w planach Pro i wyższych.

Ile kosztuje kod wysłany za granicę?

smsportal nie pobiera opłaty za wiadomość. Kod do kraju Twojej karty SIM kosztuje tyle, co SMS w abonamencie, często nic. Kod za granicę kosztuje tyle, ile Twój operator liczy za SMS międzynarodowy.

Czy SMS z własnego numeru to bezpieczne 2FA?

Lepsze niż samo hasło, słabsze niż aplikacja TOTP czy klucz dostępu, bo numer można przejąć przez SIM swap. Do małego panelu wystarczy. Szczegóły i porównanie metod są w artykule o SMS OTP.

A jeśli używam Better Auth albo własnego backendu?

Zasada jest ta sama: Twój kod generuje OTP i wywołuje `send-sms` z nagłówkiem `x-api-key`. Gotowy przykład w Node.js jest w artykule o SMS OTP.

Źródła

  1. Supabase: Send SMS Hook
  2. Supabase: Auth Hooks (podpis, błędy, limit czasu)
  3. Supabase: Phone Login
  4. Supabase: Rate limits
  5. Standard Webhooks
  6. smsportal: dokumentacja API

Więcej w poradnikach

Wyślij pierwszego SMS-a jeszcze dziś

Załóż darmowe konto, podłącz telefon i sprawdź, jak klienci reagują na wiadomości z Twojego numeru.