skills/auth-supabase-password/SKILL.md
Authenticate with Supabase email/password for testing. Use when project.json has authentication.provider: supabase and authentication.method: email-password. Triggers on: supabase password login, email password auth, supabase credentials.
npx skillsauth add mdmagnuson-creator/yo-go auth-supabase-passwordInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Authenticate with a Supabase-powered application using email and password for testing purposes.
Project configuration in docs/project.json:
{
"authentication": {
"method": "email-password",
"provider": "supabase",
"testUser": {
"mode": "fixed",
"emailVar": "TEST_EMAIL",
"emailDefault": "[email protected]",
"passwordVar": "TEST_PASSWORD"
},
"routes": {
"login": "/login",
"authenticated": "/dashboard"
}
}
}
Environment variables in .env.local:
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJ...
[email protected]
TEST_PASSWORD=your-test-password
Test user exists in Supabase Auth with confirmed email (for fixed mode)
project.jsonimport * as fs from 'fs';
import * as path from 'path';
interface AuthConfig {
method: string;
provider: string;
testUser: {
mode: 'fixed' | 'dynamic';
emailVar?: string;
emailDefault?: string;
emailPattern?: string;
passwordVar?: string;
passwordDefault?: string;
};
routes: {
login: string;
authenticated: string;
};
selectors?: {
emailInput?: string;
passwordInput?: string;
submitButton?: string;
};
}
function loadAuthConfig(projectRoot: string): AuthConfig {
const projectJsonPath = path.join(projectRoot, 'docs', 'project.json');
const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
if (!projectJson.authentication) {
throw new Error('No authentication config found in project.json. Run /setup-auth first.');
}
return projectJson.authentication;
}
function getTestCredentials(config: AuthConfig): { email: string; password: string } {
let email: string;
let password: string;
if (config.testUser.mode === 'dynamic') {
const uuid = crypto.randomUUID().slice(0, 8);
const pattern = config.testUser.emailPattern || 'test-{uuid}@example.com';
email = pattern.replace('{uuid}', uuid);
// Dynamic mode uses a known password for all test users
password = process.env[config.testUser.passwordVar || 'TEST_PASSWORD']
|| config.testUser.passwordDefault
|| 'Test123!@#';
} else {
// Fixed mode
const emailVar = config.testUser.emailVar || 'TEST_EMAIL';
const passwordVar = config.testUser.passwordVar || 'TEST_PASSWORD';
email = process.env[emailVar] || config.testUser.emailDefault || '[email protected]';
password = process.env[passwordVar] || config.testUser.passwordDefault || '';
if (!password) {
throw new Error(
`Password not configured. Set ${passwordVar} in .env.local or ` +
`testUser.passwordDefault in project.json`
);
}
}
return { email, password };
}
function loadEnv(projectRoot: string): void {
const envPath = path.join(projectRoot, '.env.local');
if (fs.existsSync(envPath)) {
const content = fs.readFileSync(envPath, 'utf-8');
content.split('\n').forEach(line => {
const match = line.trim().match(/^([^=]+)=(.*)$/);
if (match && !process.env[match[1]]) {
process.env[match[1]] = match[2];
}
});
}
}
import { createClient, SupabaseClient } from '@supabase/supabase-js';
function getSupabaseServiceClient(): SupabaseClient {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const key = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!url || !key) {
throw new Error(
'Missing Supabase env vars. Ensure NEXT_PUBLIC_SUPABASE_URL and ' +
'SUPABASE_SERVICE_ROLE_KEY are set in .env.local'
);
}
return createClient(url, key, {
auth: { autoRefreshToken: false, persistSession: false }
});
}
async function createTestUser(
supabase: SupabaseClient,
email: string,
password: string
): Promise<void> {
const { error } = await supabase.auth.admin.createUser({
email,
password,
email_confirm: true // Auto-confirm email for test users
});
if (error && !error.message.includes('already exists')) {
throw new Error(`Failed to create test user: ${error.message}`);
}
}
import { Page } from 'playwright';
interface AuthResult {
success: boolean;
email: string;
error?: string;
}
async function authenticateWithSupabasePassword(
page: Page,
baseUrl: string,
projectRoot: string
): Promise<AuthResult> {
// Load config and env
loadEnv(projectRoot);
const config = loadAuthConfig(projectRoot);
const { email, password } = getTestCredentials(config);
try {
// Create dynamic user if needed
if (config.testUser.mode === 'dynamic') {
const supabase = getSupabaseServiceClient();
await createTestUser(supabase, email, password);
console.log(`Created dynamic test user: ${email}`);
}
// Navigate to login
await page.goto(`${baseUrl}${config.routes.login}`);
// Get selectors (use defaults or custom from config)
const selectors = config.selectors || {};
const emailSelector = selectors.emailInput || 'input[type="email"], input[name="email"]';
const passwordSelector = selectors.passwordInput || 'input[type="password"], input[name="password"]';
const submitSelector = selectors.submitButton || 'button:has-text("Sign in"), button:has-text("Log in"), button[type="submit"]';
// Wait for login form
await page.waitForSelector(emailSelector);
// Enter credentials
await page.fill(emailSelector, email);
await page.fill(passwordSelector, password);
// Submit
await page.click(submitSelector);
// Wait for authenticated page
await page.waitForURL(new RegExp(config.routes.authenticated), { timeout: 10000 });
console.log(`Authentication successful, landed on ${config.routes.authenticated}`);
return { success: true, email };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`Authentication failed: ${message}`);
return { success: false, email, error: message };
}
}
Use this template for screenshot capture or E2E setup:
import { chromium } from 'playwright';
import { createClient } from '@supabase/supabase-js';
import * as fs from 'fs';
import * as path from 'path';
// === CONFIGURATION ===
const PROJECT_ROOT = process.cwd();
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:5001';
// === HELPER FUNCTIONS ===
// (Include all functions from Steps 1-4 above)
// === MAIN ===
async function main() {
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
try {
const result = await authenticateWithSupabasePassword(page, BASE_URL, PROJECT_ROOT);
if (!result.success) {
console.error('Authentication failed:', result.error);
process.exit(1);
}
console.log(`Logged in as ${result.email}`);
// Now you can navigate to authenticated pages
// await page.goto(`${BASE_URL}/dashboard`);
// await page.screenshot({ path: 'dashboard.png' });
} finally {
await browser.close();
}
}
main().catch(console.error);
When testUser.mode is "dynamic", the skill creates a new user for each test run:
{
"authentication": {
"method": "email-password",
"provider": "supabase",
"testUser": {
"mode": "dynamic",
"emailPattern": "test-{uuid}@testmail.example.com",
"passwordDefault": "Test123!@#"
},
"cleanup": {
"enabled": true,
"trigger": "auto",
"pattern": "test-*@testmail.example.com"
}
}
}
For dynamic mode:
cleanup to remove test users after runsIf your login form uses non-standard elements:
{
"authentication": {
"selectors": {
"emailInput": "input#login-email",
"passwordInput": "input#login-password",
"submitButton": "button.login-btn"
}
}
}
Customize which env vars hold credentials:
{
"authentication": {
"testUser": {
"mode": "fixed",
"emailVar": "E2E_USER_EMAIL",
"passwordVar": "E2E_USER_PASSWORD"
}
}
}
Run /setup-auth to configure authentication, or manually add the authentication section to docs/project.json.
Ensure .env.local contains:
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJ...
Set the password via environment variable (recommended) or in project.json:
TEST_PASSWORD=your-secure-password
routes.authenticated matches where your app redirects after login| Feature | Email/Password | Passwordless OTP | |---------|----------------|------------------| | Setup complexity | Lower | Higher (need OTP retrieval) | | Test user creation | User + password | User only | | Login speed | Single form | Two-step (email, then code) | | Production similarity | May differ | Often identical | | Security in tests | Password in env | Code from database |
Choose email/password when:
When authentication.reuseSession is true, save and reuse auth state across tests for faster execution.
{
"authentication": {
"method": "email-password",
"provider": "supabase",
"reuseSession": true
}
}
import { BrowserContext, Browser } from 'playwright';
import * as fs from 'fs';
import * as path from 'path';
const AUTH_STATE_FILE = '.tmp/auth-state.json';
async function saveAuthState(context: BrowserContext): Promise<void> {
const state = await context.storageState();
fs.mkdirSync(path.dirname(AUTH_STATE_FILE), { recursive: true });
fs.writeFileSync(AUTH_STATE_FILE, JSON.stringify(state, null, 2));
console.log(`Auth state saved to ${AUTH_STATE_FILE}`);
}
async function loadAuthState(context: BrowserContext): Promise<boolean> {
if (!fs.existsSync(AUTH_STATE_FILE)) {
return false;
}
try {
const state = JSON.parse(fs.readFileSync(AUTH_STATE_FILE, 'utf-8'));
const now = Date.now() / 1000;
// Check if auth cookies are expired
const isExpired = state.cookies?.some((c: any) =>
c.name.includes('auth') && c.expires && c.expires < now
);
if (isExpired) {
console.log('Saved auth state expired');
fs.unlinkSync(AUTH_STATE_FILE);
return false;
}
await context.addCookies(state.cookies || []);
console.log('Loaded cached auth state');
return true;
} catch {
return false;
}
}
async function getAuthenticatedContext(browser: Browser): Promise<BrowserContext> {
const config = loadAuthConfig(PROJECT_ROOT);
const context = await browser.newContext();
if (config.reuseSession && await loadAuthState(context)) {
// Verify session is still valid
const page = await context.newPage();
await page.goto(`${BASE_URL}${config.routes?.authenticated || '/dashboard'}`);
if (!page.url().includes(config.routes?.login || '/login')) {
await page.close();
return context; // Session is valid
}
await page.close();
}
// Authenticate fresh
const page = await context.newPage();
await authenticateWithSupabasePassword(page, BASE_URL, PROJECT_ROOT);
if (config.reuseSession) {
await saveAuthState(context);
}
return context;
}
| Approach | Login time | |----------|------------| | Fresh login each test | ~2-3 seconds | | Session reuse | ~0 seconds |
This skill is used by:
screenshot - for capturing authenticated page screenshotsui-tester-playwright - for E2E test authenticationqa-browser-tester - for QA testing authenticated flowsauth-headless - can use this skill's API auth patternAgents should:
project.json has authentication configprovider is supabase and method is email-passwordauthenticateWithSupabasePassword() before accessing protected pagesdata-ai
Generate verification contracts before delegating tasks to sub-agents, defining how success will be measured. Triggers on: verification contract, delegation contract, task verification, contract-first delegation.
testing
Verify that Vercel environment variables point to the correct Supabase project for each environment to prevent staging/production cross-wiring. Triggers on: vercel supabase check, environment alignment, env var check, supabase environment.
development
Manage codebase and database vectorization for semantic search. Use when initializing, refreshing, or querying the vector index. Triggers on: vectorize init, vectorize refresh, vectorize search, semantic search, vector index, enable vectorization.
testing
Patterns for XCUITest UI tests for native Apple apps (macOS/iOS). Use when writing or reviewing XCUITest tests for Swift apps. Triggers on: XCUITest, xcuitest, native app testing, Apple UI tests, SwiftUI tests, AppKit tests, UIKit tests.