Supabase phone login with ChinqIT
Send Supabase login codes by SMS with ChinqIT.
Send Supabase Auth login codes by SMS with ChinqIT, using one small function.
Before you start
- A ChinqIT API key, created under API keys in the ChinqIT dashboard.
- The Supabase CLI, linked to your project.
Create the function
supabase functions new chinqit-smsReplace the content of supabase/functions/chinqit-sms/index.ts with this code:
// Supabase "Send SMS" hook: checks the Supabase signature, then sends the
// login code as an SMS through ChinqIT.
// Env vars: CHINQIT_API_KEY, SEND_SMS_HOOK_SECRET, CHINQIT_SMS_LANGUAGE (fr, en or ar; default fr).
import { Webhook } from "npm:standardwebhooks@1.0.0";
const TEXTS: Record<string, (otp: string) => string> = {
fr: (otp) => `Votre code ChinqIT est ${otp}`,
en: (otp) => `Your ChinqIT code is ${otp}`,
ar: (otp) => `رمز ChinqIT الخاص بك هو ${otp}`,
};
// Supabase only reads the error message when the hook answers 200, so refusals
// go out as 200 with the real code inside. 429 and 503 stay real statuses so
// Supabase retries.
const reply = (status: number, message?: string) => {
if (message) console.error(message);
const retry = status === 429 || status === 503;
return new Response(JSON.stringify(message ? { error: { http_code: status, message } } : {}), {
status: message && !retry ? 200 : status,
headers: { "Content-Type": "application/json", ...(retry && { "retry-after": "true" }) },
});
};
export async function handler(req: Request): Promise<Response> {
const secret = Deno.env.get("SEND_SMS_HOOK_SECRET")!.replace(/^v1,/, "").replace(/^whsec_/, "");
const payload = await req.text();
let data: { user: { phone: string }; sms: { otp: string } };
try {
data = new Webhook(secret).verify(payload, Object.fromEntries(req.headers)) as typeof data;
} catch {
return reply(401, "Invalid signature");
}
const phone = data.user.phone.startsWith("+") ? data.user.phone : `+${data.user.phone}`;
const text = TEXTS[Deno.env.get("CHINQIT_SMS_LANGUAGE") ?? "fr"] ?? TEXTS.fr;
try {
const res = await fetch("https://sms.chinqit.com/api/v2/notify", {
method: "POST",
headers: { "x-api-key": Deno.env.get("CHINQIT_API_KEY")!, "Content-Type": "application/json" },
body: JSON.stringify({ phoneNumber: phone, message: text(data.sms.otp) }),
signal: AbortSignal.timeout(4000),
});
if (res.ok) return reply(200);
const err = await res.json().catch(() => ({}));
const code = err.code ?? err.errorCode;
const message = err.message ?? "ChinqIT refused the message";
return reply(res.status, code ? `${code}: ${message}` : message);
} catch {
return reply(503, "Could not reach ChinqIT, try again");
}
}
Deno.serve(handler);Secrets
supabase secrets set CHINQIT_API_KEY=your_api_key SEND_SMS_HOOK_SECRET=v1,whsec_xxxYou get SEND_SMS_HOOK_SECRET in the "Enable the hook" step. To change the SMS language, add CHINQIT_SMS_LANGUAGE=fr, en or ar (fr by default).
Deploy
supabase functions deploy chinqit-sms --no-verify-jwtEnable the hook
In Authentication → Sign In / Providers, enable Phone.
- In Supabase, open Authentication → Hooks → Send SMS hook and choose HTTPS.
- Enter the URL
https://<project-ref>.supabase.co/functions/v1/chinqit-sms. - Generate the secret, save it as
SEND_SMS_HOOK_SECRETwithsupabase secrets set, then redeploy the function.
Rate limit
In Authentication → Rate Limits, Supabase allows 30 SMS per hour by default. Raise it to match the number of logins you expect.
Test
await supabase.auth.signInWithOtp({ phone: '+22238089336' })
// After the SMS arrives
await supabase.auth.verifyOtp({ phone: '+22238089336', token: '123456', type: 'sms' })If it fails
The error appears in the response of signInWithOtp and in the function logs (Edge Functions → chinqit-sms → Logs), as CODE: message. Then see Troubleshooting.