PlaywrightSupabaseGitHub Actions
Testing Supabase Auth Email Flows with Playwright
Test Supabase email verification, OTP codes and magic links in real CI pipelines — without Docker, InBucket, or a local Supabase stack. Tests run against your real Supabase project.
InBucket vs ZeroDrop
InBucket is great for local development — it ships with supabase start and works well there. But in CI, it requires Docker, a full local Supabase stack, and doesn't test against your real Supabase project.
ZeroDrop works against your real Supabase project in real CI — no Docker service block, no local stack.
Install
npm install zerodrop-client @supabase/supabase-js
Email verification (OTP flow)
import { test, expect } from "@playwright/test";
import { ZeroDrop } from "zerodrop-client";
const mail = new ZeroDrop();
test("Supabase signup with OTP verification", async ({ page }) => {
const inbox = mail.generateInbox();
await page.goto("/signup");
await page.fill('[name="email"]', inbox);
await page.fill('[name="password"]', "TestPassword123!");
await page.click('[type="submit"]');
// Supabase sends the verification email
// ZeroDrop catches it in <1s
const email = await mail.waitForLatest(inbox, { timeout: 15000 });
// OTP auto-extracted — no regex needed
expect(email.otp).toBeTruthy();
await page.fill('[name="otp"]', email.otp!);
await page.click('[type="submit"]');
await expect(page).toHaveURL("/dashboard");
});Magic link login
test("Supabase magic link login", async ({ page }) => {
const inbox = mail.generateInbox();
await page.goto("/login");
await page.fill('[name="email"]', inbox);
await page.click('button:has-text("Send magic link")');
const email = await mail.waitForLatest(inbox, { timeout: 15000 });
// magicLink auto-extracted
expect(email.magicLink).toBeTruthy();
await page.goto(email.magicLink!);
await expect(page).toHaveURL("/dashboard");
});Password reset
test("Supabase password reset", async ({ page }) => {
const inbox = mail.generateInbox();
await page.goto("/forgot-password");
await page.fill('[name="email"]', inbox);
await page.click('[type="submit"]');
const email = await mail.waitForLatest(inbox, { timeout: 15000 });
await page.goto(email.magicLink!);
await page.fill('[name="password"]', "NewPassword123!");
await page.click('[type="submit"]');
await expect(page).toHaveURL("/login");
});GitHub Actions CI — no Docker
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}No services: block. No supabase start. Tests run against your real Supabase project.
What you're actually testing
✓Supabase Auth email delivery is working correctly
✓OTP codes are valid and accepted
✓Magic links contain valid Supabase callback URLs
✓Password reset flow works end to end
✓Your real Supabase project — not a local mock
Ready to test your Supabase auth flows?
Free tier. No signup. No Docker. Works in CI in 5 minutes.