Testing recipe
Test Supabase phone authentication with real SMS
Keep Supabase as the authentication and SMS sender. Receive its verification message at your Phonekit test number, submit that code to verifyOtp, and check that Supabase created the intended user session.
Configure the Supabase environment you use
For hosted Supabase, enable the phone provider in the dashboard's Auth Providers settings and configure the project's SMS sender. For local CLI development, use the auth.sms settings in supabase/config.toml. Self-hosted Docker installations use their own Auth environment settings. These are distinct configuration paths. Hosted phone sign-in, CLI configuration, self-hosted phone configuration.
For this real-receipt test, use a number outside any fixed test-OTP mapping. A mapped test number can exercise session creation while skipping delivery. Keep that fast coverage and add this receipt test for the sender path you need to validate.
Prepare an existing staging account
Install the server-side JS/TS SDK in your test project. Install it with npm install @phonekit/sdk. Keep its key in the test runner environment.
Install @supabase/supabase-js and Playwright in the test project. Set SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, PHONEKIT_API_KEY, and TEST_PHONE_NUMBER. Use the publishable client key for the sign-in flow; reserve any admin account setup/reset for a separate staging fixture. The example uses shouldCreateUser: false, so the test account must already exist.
npm install --save-dev @playwright/test @supabase/supabase-jsUse the Playwright config with one worker and a 180-second test deadline. This provider test does not require a browser or application UI.
Assert the normal verification result
import { test, expect } from "@playwright/test"
import { createClient } from "@supabase/supabase-js"
import { Phonekit } from "@phonekit/sdk"
function required(name: string): string {
const value = process.env[name]
if (!value) throw new Error(`Set ${name}`)
return value
}
test("Supabase accepts the real SMS and creates a session", async () => {
const phone = required("TEST_PHONE_NUMBER")
const number = new Phonekit({ apiKey: required("PHONEKIT_API_KEY") }).number(
phone
)
const supabase = createClient(
required("SUPABASE_URL"),
required("SUPABASE_PUBLISHABLE_KEY"),
{ auth: { persistSession: false, autoRefreshToken: false } }
)
const after = await number.mark()
const sent = await supabase.auth.signInWithOtp({
phone,
options: { shouldCreateUser: false, channel: "sms" },
})
if (sent.error) throw new Error("Supabase rejected the send request")
const token = await number.waitForOtp({ after, timeoutMs: 120_000 })
let failure: unknown
let failed = false
let cleanupFailed = false
try {
const verified = await supabase.auth.verifyOtp({
phone,
token,
type: "sms",
})
if (verified.error) throw new Error("Supabase rejected the received code")
expect(verified.data.session !== null).toBe(true)
expect(
verified.data.user?.phone?.replace(/^\+/, "") === phone.replace(/^\+/, "")
).toBe(true)
} catch (error) {
failed = true
failure = error
} finally {
try {
const signedOut = await supabase.auth.signOut({ scope: "local" })
cleanupFailed = signedOut.error !== null
} catch {
cleanupFailed = true
}
}
if (failed) throw failure
if (cleanupFailed) throw new Error("Supabase test session cleanup failed")
})npx playwright test tests/supabase-phone.spec.ts --workers=1This test checks the configured Supabase phone-auth service through session creation. It does not exercise your browser labels, redirects or protected pages. Add the browser recipe through your application for that layer.
Diagnose sender, receipt and session separately
A failed send request is a Supabase/provider configuration or policy result. A successful send followed by a receipt timeout needs sender-delivery and number checks. A received code rejected by verifyOtp needs validity, attempt and phone-identity checks. The test reports those stages without attaching raw provider responses or sessions to logs.
Respect the project's configured cooldown, OTP validity, CAPTCHA and request limits. If your app requires CAPTCHA, exercise that application path rather than adding a bypass for this direct provider test. Keep account reset distinct from local session sign-out; the finally block clears the client session, not the test user's server-side application records.
Common questions
Can I keep fixed Supabase test OTPs?
Yes. Keep fixed-code tests for fast application and session coverage, then use an unmapped Phonekit number for the path intended to exercise delivery. Check the mapping in the configuration for your hosted, local or self-hosted environment.
Does this example create a new user every run?
No. shouldCreateUser: false requires an existing test account. Prepare that account outside the sign-in test. Choose a separate registration fixture when the behavior you want to verify is account creation.
Do I need the Supabase service-role key for this test?
No. The sign-in and verification calls use the project’s publishable client key. If your staging fixture resets accounts with privileged operations, keep that credential in the separate server-side fixture.
Does signOut delete the account or revoke every session?
The example requests local sign-out for this client session. It does not delete the user or reset your application’s stored account state. Use a deliberate staging cleanup step for those records.
Why does the browser still fail when this provider test passes?
This test ends at Supabase session creation. Browser state, callback routing and protected UI are another layer. Run the application’s browser flow with the same receiver approach and assert the protected state you expect.