Playwright can test Supabase passwordless authentication by requesting an email link or one-time password, retrieving it from a controlled test inbox, completing the callback, and asserting a protected page. Match the email by recipient and request time so the test never consumes an older message.
Supabase supports password, email link, OTP, social login, SSO, and MFA through Supabase Auth. Its documentation explains that email links and OTPs share the same implementation, with the email template deciding whether the user receives a URL or a code.
Key takeaways
- Use a mailbox with an API rather than trying to automate a consumer webmail UI.
- Match each inbox message to a dedicated recipient and the current request time.
- Verify the final application session, not only the email confirmation page.
- Test expired and reused codes in CI, not in a frequent production monitor.
- Keep inbox tokens and Supabase credentials in environment variables.
What does a passwordless test need to cover?
A complete email-link or OTP login involves four systems:
- Your sign-in form calls Supabase Auth.
- Supabase creates the passwordless request and sends an email.
- The message reaches the configured mail provider.
- The link or code returns the browser to your application with a valid session.
Calling signInWithOtp() directly only covers the second step. Opening a protected page with a seeded token covers the fourth. The browser and inbox flow is the test that covers all four.
Configure a mailbox for automated runs
Use a test mailbox provider that exposes messages through an API. Configure a dedicated account controlled by the team and record when each request begins:
const email = process.env.SUPABASE_TEST_EMAIL
const requestedAfter = new Date().toISOString()The provider should let the script filter by recipient and arrival time. This keeps the same Supabase test user across runs instead of creating a new user for every address. Delete messages after the run if the mailbox has retention or privacy requirements.
Do not automate Gmail or another consumer inbox through a second browser tab. That introduces another login system, MFA policy, and UI into a test that is meant to diagnose Supabase.
Request the Supabase email link or OTP
This example covers a code-based form. The first half is identical for an email link:
import { test, expect } from '@playwright/test'
test('user signs in with a Supabase email OTP', async ({ page, request }) => {
test.setTimeout(90_000)
const email = process.env.SUPABASE_TEST_EMAIL
const requestedAfter = new Date().toISOString()
await test.step('Request an OTP', async () => {
await page.goto(`${process.env.APP_ORIGIN}/login`)
await page.getByLabel(/email/i).fill(email)
await page.getByRole('button', { name: /send code|continue/i }).click()
await expect(page.getByText(/check your email|code sent/i)).toBeVisible()
})
const otp = await waitForOtp(request, email, requestedAfter)
await test.step('Enter the OTP', async () => {
await page.getByLabel(/verification code|one-time code/i).fill(otp)
await page.getByRole('button', { name: /verify|sign in/i }).click()
})
await test.step('Verify the authenticated session', async () => {
await expect(page).toHaveURL(/\/account|\/dashboard/)
await expect(page.getByTestId('current-user-email')).toHaveText(email)
})
})The forms and inputs guide covers OTP inputs split across several boxes.
Poll the inbox without using fixed waits
Wrap the mailbox API in a bounded polling helper. The endpoint in this example represents your mail-capture provider or a small internal adapter:
async function waitForOtp(request, recipient, requestedAfter) {
const deadline = Date.now() + 30_000
while (Date.now() < deadline) {
const response = await request.get(process.env.TEST_INBOX_API_URL, {
headers: {
Authorization: `Bearer ${process.env.TEST_INBOX_API_TOKEN}`,
},
params: { recipient, after: requestedAfter },
})
if (response.ok()) {
const message = await response.json()
const match = message.text?.match(/\b(\d{6})\b/)
if (match) return match[1]
}
await new Promise(resolve => setTimeout(resolve, 1_000))
}
throw new Error(`No OTP received for ${recipient}`)
}Match the code length to the application's template. Return only the fields the test needs from an internal inbox adapter so mailbox administration data never appears in the Playwright trace.
Testing an email-link callback
For link-based login, extract the confirmation URL and navigate the existing page to it:
const message = await waitForEmail(request, email)
const confirmationUrl = extractConfirmationUrl(message.html)
await page.goto(confirmationUrl, { waitUntil: 'domcontentloaded' })
await expect(page).toHaveURL(`${process.env.APP_ORIGIN}/account`)
await expect(page.getByTestId('current-user-email')).toHaveText(email)Supabase determines the destination using the project's site URL and permitted redirect URLs. A production check should assert the exact application origin to catch a callback accidentally left on staging.
Supabase documents PKCE support for email links, OAuth, signup, and password recovery. If the application uses server-side rendering, ensure the callback exchanges the authorization code and writes the session cookie before the protected assertion runs.
How do you test expiry and reuse?
Keep failure cases in CI because they deliberately wait or consume credentials:
- Enter an incorrect OTP and assert the validation message.
- Reuse an already consumed code and expect rejection.
- Request a new code and verify that the intended code is accepted.
- Open a link with an invalid or expired token.
- Confirm that excessive submissions produce a useful rate-limit message.
Supabase exposes distinct auth error codes such as otp_expired, otp_disabled, and request rate-limit errors. Assert the user-facing message in the browser and log the underlying code on the server when available.
Should you seed a Supabase session for other tests?
Yes. Most application tests should avoid sending email. Authenticate through a test-only API or Supabase client, then initialize the browser with the resulting session before opening the protected page.
Keep that setup separate from the passwordless journey. A seeded session verifies authorization and protected UI, while the inbox test verifies passwordless delivery and callback behavior.
Read the cookies, localStorage, and session-state guide before injecting browser state. Supabase SSR setups commonly use cookies, while client-only applications may persist auth data in browser storage.
How to use Hyperping for Supabase passwordless monitoring
1. Configure a test inbox
Create a controlled recipient domain or inbox API. Confirm the Browser Check region can reach it without an interactive login.
2. Request the email link or OTP
Open the production sign-in page, submit the dedicated test address, and assert the application's sent-message state.
3. Retrieve the latest message
Poll the inbox API by recipient. Store the API URL and token under Hyperping environment variables.
4. Complete authentication
Enter the code or open the confirmation URL in the same Playwright page. Wait for the configured application callback.
5. Verify and schedule the check
Assert a protected element and current-user identifier. Add the script to a Hyperping Browser Check and keep the complete run below two minutes.
Separate delivery coverage from session setup
One scheduled test should prove that the passwordless email reaches a controlled inbox and creates an application session. Other browser tests can use faster session setup so they do not send an email every time.
The Playwright API testing guide shows how to combine inbox or auth API calls with browser assertions, while the authentication guide covers reusable state.
FAQ
Can Playwright test Supabase passwordless login? ▼
Yes. Playwright can submit the email form, retrieve the link or OTP from a controlled test inbox, complete authentication, and verify the resulting protected page.
How do I get a Supabase OTP during an automated test? ▼
Route test emails to a mailbox provider with an API or to a test-only mail-capture service. Poll that API for the newest message and extract the code without exposing mailbox credentials in the test source.
Should I reuse a Supabase session in Playwright? ▼
Reuse a session for tests that only need an authenticated user. Keep a separate passwordless-login test that requests and consumes a real email link or OTP.
Why does a Supabase email link work locally but fail in CI? ▼
The callback URL may be absent from Supabase's redirect allowlist, the code may expire before it is used, or the test may read an older email. Match messages by recipient and request time, then assert the final application URL.



