Better Auth
Better Auth's phone number plugin calls a
sendOTP function you write whenever a code must go out. Point it at Bridge with the
SDK:
npm i @kroszborg/bridgeSet BRIDGE_API_KEY (a bk_live_ key, or bk_test_ while developing) and BRIDGE_URL (your
Bridge server) in the environment. Bridge expects numbers in E.164 (+919876543210); use the
plugin's phoneNumberValidator option to reject anything else early.
There are two ways to do it.
Option 1: Better Auth makes the code, Bridge sends it
Better Auth generates, stores and checks the code, as it does by default. Bridge only delivers the SMS.
import { betterAuth } from 'better-auth';
import { phoneNumber } from 'better-auth/plugins';
import { Bridge } from '@kroszborg/bridge';
const bridge = new Bridge(); // reads BRIDGE_API_KEY and BRIDGE_URL
export const auth = betterAuth({
plugins: [
phoneNumber({
sendOTP: ({ phoneNumber, code }) => {
// Not awaited, as Better Auth recommends, so the response time does not depend on SMS.
bridge.messages
.send({ to: phoneNumber, message: `${code} is your Acme code. Do not share it.` })
.catch((err) => console.error('Bridge could not queue the code', err));
},
}),
],
});Keep the text short: one SMS segment is 160 GSM-7 characters. Because Bridge sees an ordinary message, the code is stored and shown in full in Bridge until message retention removes it. Use option 2 if that matters to you.
Option 2: Bridge Verify makes and checks the code
The plugin's verifyOTP option replaces Better Auth's own check. Combined with sendOTP, Bridge
Verify generates, sends and checks the code, and Bridge's message shows it
masked.
import { betterAuth } from 'better-auth';
import { phoneNumber } from 'better-auth/plugins';
import { Bridge, BridgeApiError } from '@kroszborg/bridge';
const bridge = new Bridge();
export const auth = betterAuth({
plugins: [
phoneNumber({
// Better Auth's code is ignored: Bridge makes its own and sends it.
sendOTP: ({ phoneNumber }) => {
bridge.otp
.send({ to: phoneNumber })
.catch((err) => console.error('Bridge could not send the code', err));
},
verifyOTP: async ({ phoneNumber, code }) => {
try {
const { valid } = await bridge.otp.verify({ to: phoneNumber, code });
return valid;
} catch (err) {
// 404: no code is pending for this number. 422: the input is not 4 to 10 digits.
if (err instanceof BridgeApiError && (err.status === 404 || err.status === 422)) return false;
throw err;
}
},
}),
],
});With option 2:
- The code length, lifetime, attempts and message text come from Bridge (Verify → Message and
limits), not from the plugin's
otpLength,expiresInandallowedAttempts. Better Auth's documentation does not say whether it still generates and stores its own code whenverifyOTPis set; either way, only Bridge's code reaches the user. - Bridge's resend rules apply: a new code to the same number at most every 30 seconds and 5 times an
hour.
bridge.otp.sendthen fails with429, which the example logs. Tell users to wait before asking again. - With a
bk_test_key nothing is sent. The response includes the code ((await bridge.otp.send(...)).code) for automated tests.
Both options route the SMS like any other Bridge message: through your phones, or an SMS provider if routing says so.
Auth0
Auth0 can send its SMS (passwordless codes, MFA codes, phone verification) through any gateway with a custom phone provider: an Action on the custom-phone-provider trigger that…
Firebase and Clerk
Use Bridge with Firebase Authentication (through Bridge Verify) and with Clerk (by delivering its SMS from a webhook).