This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
- Proper Solutions Over Quick Fixes - Implement correctly the first time
- Root Cause Analysis - Fix underlying issues, not symptoms
- Stability Over Speed - This is a production template
- Clean Architecture - Follow established patterns consistently
- No Technical Debt - Never commit TODOs or workarounds
Sprint 3.5 Complete ✅ - All technical debt eliminated (2025-09-19) Post-Cleanup Complete ✅ - Next.js 15 params fixed (2025-09-30)
Next Steps: Choose between PRP-001 (Methodology Documentation) or Sprint 4 (Advanced Features)
ScriptHammer uses Product Requirements Prompts (PRPs) integrated with SpecKit workflow commands for feature development.
Quick Start: See SPECKIT-PRP-GUIDE.md
Full Guide: See PRP Methodology
Workflow:
- Write PRP:
docs/prp-docs/<feature>-prp.md - Create branch:
./scripts/prp-to-feature.sh <feature> <number> - Run SpecKit:
/specify→/plan→/tasks→/implement
When to Use: Features taking >1 day, external integrations, architectural changes
CRITICAL: Components must follow the 5-file pattern or CI/CD will fail:
ComponentName/
├── index.tsx # Barrel export
├── ComponentName.tsx # Main component
├── ComponentName.test.tsx # Unit tests (REQUIRED)
├── ComponentName.stories.tsx # Storybook (REQUIRED)
└── ComponentName.accessibility.test.tsx # A11y tests (REQUIRED)
Always use the generator: docker compose exec scripthammer pnpm run generate:component
docker compose up # Start development environment
docker compose exec scripthammer pnpm run dev # Dev server on :3000
docker compose exec scripthammer pnpm test # Run tests
docker compose exec scripthammer pnpm run storybook # Start Storybook
docker compose exec scripthammer pnpm run docker:clean # Clean start if issuesInteractive mode (recommended for learning):
docker compose exec scripthammer pnpm run generate:component
# Prompts for: name, category, hasProps, includeHooksCLI mode (for scripting/automation):
docker compose exec scripthammer pnpm run generate:component -- \
--name ComponentName \
--category atomic \
--hasProps true \
--withHooks false
# Categories: subatomic, atomic, molecular, organisms, templatesdocker compose exec scripthammer pnpm run test:suite # 🧪 Comprehensive test suite
docker compose exec scripthammer pnpm run test:quick # ⚡ Quick validation
docker compose exec scripthammer pnpm run type-check # TypeScript checking
docker compose exec scripthammer pnpm run lint # ESLint checking
docker compose exec scripthammer pnpm run test:coverage # Coverage report
docker compose exec scripthammer pnpm run test:a11y:dev # Accessibility testingThe test:suite command runs all tests and provides colored output with:
- TypeScript type checking
- ESLint validation
- Code formatting check
- Unit tests with coverage
- Component structure validation
- Production build test
- Accessibility tests (if dev server is running)
The project uses Product Requirements Prompts for feature implementation:
# Step 1: Setup feature branch (run from host machine)
./scripts/prp-to-feature.sh <prp-name> <number>
# Step 2: Generate plan (tell Claude)
"execute /plan"
# Step 3: Generate tasks (tell Claude)
"execute /tasks"Remaining PRPs (0.3.0) - Priority Order:
PRP-001: PRP Methodology✅ Completed (merged before blog posts)PRP-010: EmailJS Integration✅ CompletedPRP-011: PWA Background Sync✅ CompletedPRP-013: Calendar Integration✅ CompletedPRP-014: Geolocation Map✅ Completed (2025-09-18)Auto-Configuration System✅ Completed (2025-09-18) - Auto-detects project name from git remote (not a formal PRP)Lighthouse Phases 3 & 4✅ Completed (2025-09-30) - Best Practices 100/100, PWA deprecated in Lighthouse 12.0PRP-017: Mobile-First Design✅ Completed (2025-10-01) - Full mobile-first overhaul with 44px touch targetsPRP-015: Payment Integration✅ Completed (2025-10-03) - Supabase backend, Stripe/PayPal providers, GDPR consent, offline queue- PRP-012: Visual Regression Testing (P2 - deferred until UI stable)
Future PRPs (0.4.0):
- PRP-015: Enhanced Geolocation Accuracy (P2 - hybrid desktop/mobile approach)
Current Scores (verified via Lighthouse CLI):
- ✅ Performance: 95/100
- ✅ Accessibility: 96/100
- ✅ Best Practices: 100/100 (all console statements removed)
- ✅ SEO: 100/100
⚠️ PWA: N/A (scoring deprecated in Lighthouse 12.0, May 2024)
PWA Status: Despite no Lighthouse score, the app IS a fully functional PWA:
- Service worker registered and active
- Valid web app manifest
- Served over HTTPS
- Meets all Chrome installability criteria
- Offline support enabled
Note: PWA scoring was completely removed from Lighthouse 12.0+. Use Chrome DevTools → Application tab to verify PWA installability.
- Next.js 15.5.2 with App Router, static export
- React 19.1.0 with TypeScript strict mode
- Tailwind CSS 4 + DaisyUI (32 themes)
- PWA with Service Worker (offline support)
- Testing: Vitest (58% coverage), Playwright E2E
- CI/CD: GitHub Actions with Husky hooks
src/
├── app/ # Next.js pages
├── components/ # Atomic design pattern
│ ├── subatomic/ # Primitives
│ ├── atomic/ # Basic components (includes CalendarEmbed)
│ ├── calendar/ # Calendar providers and consent
│ └── privacy/ # GDPR components
├── config/ # Project configuration
├── contexts/ # React contexts
└── utils/ # Utilities
- Unit Tests: Vitest + React Testing Library (25% minimum)
- E2E Tests: Playwright (40+ tests, local only)
- Accessibility: Pa11y CI configured
- Visual: Chromatic package installed
- Coverage: Currently 58%, target 60%
- Minimum Size: 44×44px (WCAG AAA / Apple HIG compliance)
- Apply to ALL: Buttons, links, form inputs, interactive icons
- Implementation: Use
min-h-11 min-w-11utility classes (Tailwind: 44px = 11 × 4px)
// ✅ CORRECT - Mobile-first touch targets
<button className="btn btn-primary min-h-11 min-w-11">Click Me</button>
<Link href="/page" className="inline-block min-h-11 min-w-11">Link</Link>
<input className="input input-bordered min-h-11" />
// ❌ WRONG - Touch targets too small for mobile
<button className="btn btn-xs">Too Small</button>
<a href="/page" className="text-sm">No touch target</a>Use mobile-first progressive enhancement for padding/margins:
// Container padding: mobile → tablet → desktop
<div className="px-4 py-6 sm:px-6 sm:py-8 md:py-12 lg:px-8">
// Vertical rhythm: progressively increase spacing
<header className="mb-6 sm:mb-8 md:mb-10 lg:mb-12">
// Gap spacing: start small, grow larger
<div className="flex gap-2 sm:gap-3 md:gap-4">Stack on mobile, horizontal on tablet+:
// Cards, forms, navigation
<div className="flex flex-col gap-4 sm:flex-row sm:gap-6">
// Card side-by-side layout
<Card side> {/* Automatically applies md:card-side - stacks on mobile */}Mobile-first breakpoints (defined in src/config/breakpoints.ts):
- xs: 320px (minimum supported - iPhone SE)
- sm: 428px (standard phones - iPhone 14 Pro Max)
- md: 768px (tablets - iPad Mini)
- lg: 1024px (desktop - iPad Pro landscape)
- xl: 1280px (large desktop)
Use the useDeviceType hook for runtime device awareness:
import { useDeviceType } from '@/hooks/useDeviceType';
function MyComponent() {
const device = useDeviceType();
if (device.category === 'mobile') {
// Mobile-specific behavior
}
// Access: width, height, breakpoint, category, orientation, hasTouch
}Playwright is configured with 8 mobile + 2 tablet viewports:
docker compose exec scripthammer pnpm exec playwright test
# Tests automatically run against all configured mobile viewportsNavigation controls:
<div className="flex items-center gap-0.5 sm:gap-1 md:gap-2">
<button className="btn btn-ghost btn-circle min-h-11 min-w-11">Blog/article content:
<article className="container mx-auto px-4 py-6 sm:px-6 sm:py-8 md:py-12 lg:px-8">
<div className="max-w-3xl"> {/* Constrain line length for readability */}Modal/dialog close buttons:
<button className="btn btn-circle btn-xs sm:btn-sm min-h-11 min-w-11">✕</button>NEW: Auto-Configuration! The project name is now automatically detected from your GitHub fork!
UpdateAuto-detected from git remote/src/config/project-status.jsonwith your project infoReplace GitHub Pages URLs in configsAuto-configured- Copy
.env.exampleto.envand set your UID/GID (required for Docker) UpdateAuto-detectedbasePathinnext.config.tsif needed
# Copy the example file
cp .env.example .env
# Get your system's UID and GID
echo "UID=$(id -u)" >> .env
echo "GID=$(id -g)" >> .envSee .env.example for all available environment variables including:
- Google Analytics, EmailJS, Web3Forms integration
- Calendar providers (Calendly/Cal.com)
- Author information and social links
- Project overrides for special deployment scenarios
- Test user credentials for contract tests
See /docs/TEMPLATE-GUIDE.md for details on the auto-configuration system.
Contract tests require pre-configured test users in Supabase:
Primary Test User (required):
- Email:
test@example.com(or override withTEST_USER_PRIMARY_EMAIL) - Password:
TestPassword123!(or override withTEST_USER_PRIMARY_PASSWORD) - Must be pre-created and email-confirmed in Supabase (see
/supabase/seed-test-user.sql) - Used for: sign-in, profile operations, standard contract tests
Secondary Test User (optional - for email verification tests):
- Configure in
.env:TEST_USER_SECONDARY_EMAILandTEST_USER_SECONDARY_PASSWORD - Must be a REAL email address you control (e.g.,
yourname+scripthammer@gmail.com) - Enables: password reset tests, sign-up flow tests with actual email verification
- Tests will be gracefully skipped if not configured
To enable all contract tests:
# Add to .env
TEST_USER_SECONDARY_EMAIL=yourname+scripthammer@gmail.com
TEST_USER_SECONDARY_PASSWORD=YourTestPassword123!Note: Gmail's + addressing is perfect for test users - all emails go to your main inbox with different addresses.
For CI/CD pipelines to work correctly, you must configure the following secrets in your GitHub repository.
Quick setup: → Add secrets to this repository
🚀 EASIEST METHOD - Copy from your .env file:
If you already have a working .env file, just copy the values directly from there! The GitHub secrets should be exactly the same as your local environment variables.
Required secrets:
| GitHub Secret Name | Copy from .env variable |
What it is |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
NEXT_PUBLIC_SUPABASE_URL |
Full URL (e.g., https://xxxxx.supabase.co) |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Long JWT token starting with eyJ... |
SUPABASE_SERVICE_ROLE_KEY |
SUPABASE_SERVICE_ROLE_KEY |
Long JWT token starting with eyJ... |
NEXT_PUBLIC_DEPLOY_URL |
NEXT_PUBLIC_DEPLOY_URL (or use your actual URL) |
https://scripthammer.com |
Steps:
- Open your
.envfile - Click → Add secrets to GitHub
- For each secret above:
- Click "New repository secret"
- Name: Use the "GitHub Secret Name" from the table
- Value: Copy-paste the entire value from your
.envfile (the full JWT token, not just a snippet) - Click "Add secret"
Don't have a .env file? Get the values from Supabase:
- Go to Supabase Dashboard
- Select your project
- Click ⚙️ Settings (gear icon) → API
- Copy the full values from "Project API keys" section
SUPABASE_SERVICE_ROLE_KEY bypasses Row Level Security. Never expose it in client code or commit it to the repository. It's only used for integration test database cleanup in GitHub Actions.
Workflows that require these secrets:
.github/workflows/ci.yml- Main CI pipeline (tests, build).github/workflows/accessibility.yml- Accessibility testing pipeline
Double defense implemented for bulletproof .next handling:
- ✅ Entrypoint script: Automatically cleans and recreates .next on every container start
- ✅ Named volume: Isolates .next from host filesystem (
next_cache:/app/.next) - ✅ No more
EACCESerrors - ever - ✅ Zero manual intervention required
- ✅ AI agents don't need to remember cleanup commands
- ✅ Works across all scenarios (restarts, rebuilds, branch switching)
Implementation:
- Entrypoint script:
/docker/docker-entrypoint.sh(lines 15-27) - Docker Compose:
docker-compose.yml(named volume) - Automatic on every
docker compose up
If CSS styles aren't appearing:
- Ensure Leaflet CSS import is NOT in
globals.css(causes build issues) - Import Leaflet CSS only in map components that use it
- Restart Docker container after CSS changes
pnpm run docker:clean # Clean start if neededThe .env file with UID/GID is now created from .env.example:
# Quick setup
cp .env.example .env
# The default values (UID=1000, GID=1000) work for most Linux/WSL systems
# Or set your specific values
echo "UID=$(id -u)" > .env
echo "GID=$(id -g)" >> .envdocker compose down
lsof -i :3000
kill -9 <PID>corepack enable
corepack prepare pnpm@10.16.1 --activateAfter completing remaining PRPs, these features from previous constitutions need implementation:
- Advanced Tooling: OKLCH color system scripts
- Validation: Multi-level validation patterns
- Security: Automated dependency scanning
- Performance: Bundle analysis dashboard
- State Management: Zustand/Jotai implementation
- Animations: Framer Motion integration
- UI Components: Command palette, DataTable, Modal system
- Developer Tools: Component generator CLI
See /SPRINT-4-ROADMAP.md for detailed planning.
Successfully implemented offline form submission with automatic synchronization:
- IndexedDB Queue: Persistent storage for offline submissions
- Service Worker Sync: Background sync API integration
- React Integration: Custom
useOfflineQueuehook - User Feedback: Clear UI indicators for offline state
4 integration tests fail in /src/tests/offline-integration.test.tsx due to React Hook Form async validation timing in test environment. Production functionality works correctly.
Root Cause: Complex interaction between mocked hooks and form validation lifecycle. The submit button remains disabled in tests despite valid form data.
Verification: See /docs/testing/KNOWN-TEST-ISSUES.md for detailed analysis and manual verification steps.
Future Fix: Split into focused unit tests + Playwright E2E tests for real browser validation.
Successfully implemented interactive maps with geolocation support:
- Leaflet.js Integration: Open-source maps with OpenStreetMap tiles
- GDPR Compliance: Consent modal before location access
- Dynamic Loading: SSR disabled for map components
- Known Limitation: Desktop browsers use IP-based geolocation (city-level accuracy)
- Future Enhancement: See PRP-015 for desktop accuracy improvements
- Dexie.js: TypeScript-friendly IndexedDB wrapper
- LZ-String: Text compression for 5MB storage limit
- markdown-to-jsx: React-compatible markdown rendering
- Workbox: Service Worker management for offline support
// Always use transactions for bulk operations
await db.transaction('rw', db.posts, db.images, async () => {
// All operations in same transaction
});
// Handle quota errors
try {
await db.posts.add(post);
} catch (e) {
if (e.name === 'QuotaExceededError') {
// Handle storage limit
}
}// Compress before storing
const compressed = LZString.compress(content);
// Decompress when reading
const original = LZString.decompress(compressed);# Test offline mode
docker compose exec scripthammer pnpm test:offline
# Debug service worker
chrome://inspect/#service-workers// Check IndexedDB usage
const dbs = await indexedDB.databases();
console.log('Databases:', dbs);
// Inspect Dexie tables
await db.posts.toArray();
await db.syncQueue.count();IMPORTANT: When editing blog content, ONLY edit files in /public/blog/*.md
- DO NOT EDIT:
/out/blog/*.md- These are build outputs - DO NOT EDIT:
/src/lib/blog/blog-data.json- This is generated from markdown files - ONLY EDIT:
/public/blog/*.md- The source markdown files
The build process flow:
- Source markdown files:
/public/blog/*.md - Generate JSON at build time:
pnpm run generate:blog→/src/lib/blog/blog-data.json - Static export copies to:
/out/blog/*.md(for GitHub Pages deployment)
After editing blog markdown files, regenerate the JSON:
docker compose exec scripthammer pnpm run generate:blogSuccessfully implemented a complete offline-first blog system with IndexedDB, PWA support, and background sync.
- Offline-First: Full functionality without internet using IndexedDB
- Storage Management: 5MB text, 200MB images with quota tracking
- Background Sync: Automatic synchronization when coming online
- Conflict Resolution: Smart handling of concurrent edits
- Post Scheduling: Future publication with cron-like scheduling
src/
├── app/api/blog/ # API routes (posts, sync, storage, images)
├── app/blog/ # UI pages (list, editor, schedule, [slug])
├── components/blog/ # React components (5-file pattern)
├── lib/blog/ # Core libraries (database, sync, compression)
├── services/blog/ # Business logic (post, storage, schedule, offline)
└── types/blog.ts # TypeScript interfaces
# Run blog tests
docker compose exec scripthammer pnpm test src/tests/**/blog-*.test.ts
# Check storage usage
curl http://localhost:3000/api/blog/storage
# Trigger sync manually
curl -X POST http://localhost:3000/api/blog/sync?action=process
# Clean up old content
curl -X POST http://localhost:3000/api/blog/storage?action=cleanup- IndexedDB Issues: Check DevTools > Application > IndexedDB
- Sync Problems: Monitor Network tab for failed requests
- Storage Quota: Check at
/blog/storageor API endpoint - Service Worker: Ensure registered in Application tab
- Desktop browsers use IP geolocation (city-level accuracy)
- Some integration tests fail due to React Hook Form timing (production works)
- Service worker requires HTTPS in production
- Never create components manually - use the generator
- All PRs must pass component structure validation
- Tests run on pre-push (Husky v9 shows deprecation warning - non-breaking)
- E2E tests are local-only, not in CI pipeline
- Docker-first development is mandatory for consistency
- PRP-011 has 4 known test failures that don't affect production - see test issues doc
- Blog system (Feature 018) is fully functional with 69/77 tasks complete
- Blog feature uses IndexedDB - test in multiple browsers
Successfully implemented comprehensive payment integration with Supabase backend, multiple payment providers, and offline-first architecture.
- Backend: Supabase (PostgreSQL + Edge Functions + Realtime)
- Payment Providers: Stripe, PayPal (Cash App/Chime planned)
- Client Library:
/src/lib/payments/- payment-service.ts, stripe.ts, paypal.ts, offline-queue.ts - UI Components:
/src/components/payment/- PaymentButton, PaymentConsentModal, PaymentStatusDisplay, PaymentHistory, SubscriptionManager - Database: Row Level Security (RLS) policies, webhook verification, retry schedules
- Tests: 8 E2E tests in
/e2e/payment/(T055-T062)
- GDPR Compliance: Consent modal before loading payment provider scripts
- Offline Queue: IndexedDB-based queue with automatic sync on reconnection
- Real-time Updates: Supabase realtime subscriptions for payment status
- Webhook Verification: Signature verification for Stripe/PayPal webhooks
- Failed Payment Retry: Exponential backoff with 3-day grace period
- Multi-currency: USD, EUR, GBP, CAD, AUD support
- Type Safety: Full TypeScript coverage with strict mode
- Database Migrations:
/supabase/migrations/- payment_intents, payment_results, webhook_events, subscriptions - Edge Functions:
/supabase/functions/- stripe-create-payment, stripe-webhook, paypal-create-subscription, paypal-webhook, send-receipt-email - Type Definitions:
/src/types/payment.ts - Integration Tests:
/e2e/payment/*.spec.ts(Playwright)
- Stripe.redirectToCheckout deprecated - using type assertion workaround
- Dexie return type is
Promise<unknown>(IndexedDB can return string, number, or ArrayBuffer) - RLS tests require
SUPABASE_SERVICE_ROLE_KEYenvironment variable
IMPORTANT: This is a completely NEW blog system being built from scratch. The previous blog system (Feature 018) was removed entirely. This feature creates a fresh implementation with enhanced capabilities.
- Dexie.js: Enhanced IndexedDB wrapper for offline storage
- LZ-String: Compression library for text content
- markdown-to-jsx: React-compatible markdown rendering
- gray-matter: Frontmatter parsing for metadata
- Prism.js: Syntax highlighting for code blocks
- Chokidar: File watching for hot reload
# Generate blog data from markdown files
docker compose exec scripthammer pnpm run generate:blog
# Test offline functionality
docker compose exec scripthammer pnpm test:offline
# Check storage quota
curl http://localhost:3000/api/blog/storage
# Trigger manual sync
curl -X POST http://localhost:3000/api/blog/sync?action=processpublic/blog/ # Source markdown files (EDIT HERE ONLY)
src/
├── app/api/blog/ # REST API endpoints
├── app/blog/ # Blog UI pages
├── components/blog/ # Blog components (5-file pattern)
├── lib/blog/ # Core blog libraries
├── services/blog/ # Blog business logic
└── data/authors.json # Author registry
/out/blog/ # Build output (DO NOT EDIT)
- Hybrid build/runtime markdown processing
- Three-way merge conflict resolution UI
- Automatic TOC generation with showToc flag
- Social sharing with Open Graph metadata
- Author profiles with social links
- Hot reload in <500ms for development
- Offline editing with IndexedDB
- Background sync when online
- Storage quota management (5MB text, 200MB images)
- Content compression with LZ-String
- Unified markdown content pipeline from specs 019 and 021
- Enhanced offline sync with conflict detection
- Improved social sharing integration
- Author profile system with registry
- Performance optimizations for hot reload
Successfully implemented comprehensive authentication system with Supabase, OAuth providers, and full mobile-first UI.
- Backend: Supabase Auth (@supabase/ssr for Next.js 15)
- Session Management: Cookie-based SSR sessions with automatic refresh
- OAuth Providers: GitHub and Google with signInWithOAuth
- Security: Row-Level Security (RLS), rate limiting, audit logging
- UI Components: 10 auth components following 5-file pattern
- Testing: 5 integration tests, 3 E2E tests, Pa11y accessibility
- Email/Password Auth: Sign-up, sign-in, email verification
- OAuth Integration: GitHub and Google social login
- Password Reset: Secure token-based password reset flow
- Session Management: Automatic token refresh, Remember Me (30 days)
- Rate Limiting: Brute force protection with localStorage
- Audit Logging: Security event tracking to database
- Protected Routes: Middleware-based route protection
- Account Management: Update profile, change password, delete account
- Mobile-First: 44px touch targets, responsive design
// In any client component
import { useAuth } from '@/contexts/AuthContext';
function MyComponent() {
const { user, session, loading, signOut } = useAuth();
if (loading) return <div>Loading...</div>;
if (!user) return <div>Please sign in</div>;
return <div>Welcome {user.email}</div>;
}// In page.tsx
import { AuthGuard } from '@/components/auth/AuthGuard';
export default function ProtectedPage() {
return (
<AuthGuard requireVerification={true} redirectTo="/verify-email">
<div>Protected content</div>
</AuthGuard>
);
}// middleware.ts automatically protects these routes:
const protectedRoutes = ['/profile', '/settings'];
const authRoutes = ['/sign-in', '/sign-up'];
// Authenticated users redirected from auth pages to /
// Unauthenticated users redirected to /sign-in// In sign-in page or OAuth button
const handleOAuth = async (provider: 'github' | 'google') => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});
};
// Callback route at /auth/callback handles token exchange-- user_profiles table (auto-created via trigger)
CREATE TABLE user_profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
username TEXT UNIQUE,
display_name TEXT,
bio TEXT,
avatar_url TEXT,
email_verified BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- RLS policies use auth.uid()
CREATE POLICY "Users can view own profile"
ON user_profiles FOR SELECT
USING (auth.uid() = id);
-- Audit logging
CREATE TABLE auth_audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
event_type TEXT NOT NULL,
event_data JSONB,
ip_address TEXT,
user_agent TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);- Auth Context:
/src/contexts/AuthContext.tsx - Middleware:
/src/middleware.ts - Supabase Client:
/src/lib/supabase/client.ts(client),/src/lib/supabase/server.ts(server) - Validation:
/src/lib/auth/email-validator.ts,/src/lib/auth/password-validator.ts - Rate Limiting:
/src/lib/auth/rate-limiter.ts - Components:
/src/components/auth/(SignUpForm, SignInForm, etc.) - Pages:
/src/app/sign-up/,/src/app/sign-in/,/src/app/profile/ - Integration Tests:
/tests/integration/auth/ - E2E Tests:
/e2e/auth/
✅ RESOLVED (2025-10-07):
Component generator creates boilerplate tests that don't match actual component implementations✅ FIXEDSome unit tests have TypeScript errors from template mismatches✅ FIXED- Templates now generate realistic, working tests
- Production functionality works correctly - comprehensive test coverage in place
Successfully hardened authentication and payment security with server-side rate limiting, OAuth CSRF protection, and comprehensive audit logging.
- Server-Side Rate Limiting (P0): 5 attempts/15min window, prevents brute force attacks
- OAuth CSRF Protection (P0): State token validation prevents session hijacking
- Payment Data Isolation (P0): RLS policies ensure users only see own payment data
- Email Validation (P1): TLD verification, disposable email warnings
- Metadata Validation (P1): Prototype pollution prevention for payment metadata
- Audit Logging (P1): Comprehensive security event tracking to database
- Password Strength Indicator (P2): Real-time visual feedback on password quality
- Accessibility (P3): ARIA live regions for screen reader announcements
Configuration:
- Max attempts: 5 per 15-minute window
- Applies to: sign_in, sign_up, password_reset
- Enforcement: Server-side (PostgreSQL functions)
- Lockout: 15 minutes after limit exceeded
Usage:
import {
checkRateLimit,
recordFailedAttempt,
} from '@/lib/auth/rate-limit-check';
// Check before allowing attempt
const rateLimit = await checkRateLimit(email, 'sign_in');
if (!rateLimit.allowed) {
// Show lockout message
return;
}
// Record failure after failed attempt
await recordFailedAttempt(email, 'sign_in');Flow:
- Generate state token before OAuth redirect
- Store in database with session ID
- Validate state on callback
- Mark token as used (single-use)
Implementation:
import { generateOAuthState, validateOAuthState } from '@/lib/auth/oauth-state';
// Before OAuth redirect
const stateToken = await generateOAuthState('github');
// Pass in OAuth URL
// On callback
const result = await validateOAuthState(stateFromURL);
if (!result.valid) {
throw new Error(result.error);
}RLS Policies:
- Users can only SELECT own payment_intents (via template_user_id)
- Users can only INSERT payment_intents for themselves
- Payment intents are immutable (no UPDATE)
- Payment results linked via intent_id join
Database:
-- Automatic enforcement via RLS
SELECT * FROM payment_intents WHERE template_user_id = auth.uid();Event Types:
sign_in,sign_out,sign_uppassword_change,password_reset_requestemail_verification,oauth_link,oauth_unlink
Usage:
import { logAuthEvent } from '@/lib/auth/audit-logger';
await logAuthEvent({
user_id: user.id,
event_type: 'sign_in',
event_data: { email, provider: 'email' },
success: true,
});Applied:
/supabase/migrations/20251006_security_hardening_complete.sql- rate_limit_attempts table
- oauth_states table
- check_rate_limit() function
- record_failed_attempt() function
- Payment RLS policies
- auth_audit_logs table
✅ RESOLVED (2025-10-07):
3 rate limiting tests fail due to database state (8/11 pass)✅ FIXED- Proper test pyramid now in place:
- Unit tests: Mock Supabase client (
/src/lib/auth/__tests__/rate-limit-check.unit.test.ts) - Integration tests: Real database (
/tests/integration/auth/rate-limiting.integration.test.ts) - E2E tests: Real browser (
/e2e/auth/rate-limiting.spec.ts)
- Unit tests: Mock Supabase client (
- All tests now fast, reliable, and properly isolated
Successfully implemented user avatar upload with client-side cropping, Supabase Storage integration, and mobile-first design.
- Frontend: react-easy-crop (v5.5.3) for image cropping with touch support
- Storage: Supabase Storage with 5MB limit, public read access
- Image Processing: Canvas API for WebP compression (800x800px @ 85% quality)
- RLS Policies: 4 policies ensuring user isolation (upload/update/delete own, public read)
- UI Components: AvatarUpload, AvatarDisplay (following 5-file pattern)
- Tests: Integration tests, E2E tests, accessibility tests
- Client-side cropping: Interactive crop interface with zoom control
- WebP compression: Automatic conversion to WebP format for optimal file size (~100KB)
- Initials fallback: Automatically generates initials from username/email when no avatar
- Mobile-first design: 44px touch targets, responsive layout
- Replace functionality: Automatically deletes old avatar when uploading new one
- Remove functionality: Users can remove avatar and revert to initials
- Lazy loading: All avatars use
loading="lazy"for performance - Error handling: User-friendly error messages with actionable guidance
- ARIA support: Live regions, proper labels, keyboard navigation
Upload Avatar:
import AvatarUpload from '@/components/atomic/AvatarUpload';
function MyComponent() {
const handleUploadComplete = (url: string) => {
console.log('New avatar URL:', url);
// Refresh user context or update state
};
return <AvatarUpload onUploadComplete={handleUploadComplete} />;
}Display Avatar:
import AvatarDisplay from '@/components/atomic/AvatarDisplay';
function UserProfile({ user }) {
return (
<AvatarDisplay
avatarUrl={user.user_metadata?.avatar_url}
displayName={user.user_metadata?.username || user.email}
size="lg" // sm | md | lg | xl
/>
);
}Remove Avatar:
import { removeAvatar } from '@/lib/avatar/upload';
async function handleRemove() {
const result = await removeAvatar();
if (result.error) {
console.error(result.error);
} else {
// Avatar removed successfully
}
}Storage Bucket:
-- Created via migration: 20251008_avatar_upload.sql
-- Bucket: avatars (public read, 5MB max, JPEG/PNG/WebP only)RLS Policies:
- Users can upload own avatar (INSERT)
- Users can update own avatar (UPDATE)
- Users can delete own avatar (DELETE)
- Anyone can view avatars (SELECT) - public read
Monolithic Setup:
All avatar configuration is included in /supabase/migrations/20251006_complete_monolithic_setup.sql (PART 6: STORAGE BUCKETS) for fresh setups.
Teardown:
Avatar cleanup is included in /supabase/migrations/999_drop_all_tables.sql (STEP 0) for complete database reset.
AccountSettings (/account):
- Full avatar management UI
- Upload, replace, and remove functionality
- Displays current avatar with initials fallback
GlobalNav:
- User avatar in navigation dropdown
- Uses AvatarDisplay component for consistency
UserProfileCard:
- Profile page avatar display
- Consistent appearance across app
src/
├── components/atomic/
│ ├── AvatarUpload/ # Upload component with crop modal
│ └── AvatarDisplay/ # Display component with initials fallback
├── lib/avatar/
│ ├── types.ts # TypeScript interfaces
│ ├── validation.ts # File validation (MIME, size, dimensions)
│ ├── image-processing.ts # Canvas API crop and WebP compression
│ └── upload.ts # Supabase Storage operations
tests/integration/avatar/ # Integration tests
e2e/avatar/ # E2E tests
- Desktop Compatibility: Works in all modern browsers supporting Canvas API and createImageBitmap
- File Size: 5MB hard limit (enforced by Supabase Storage bucket)
- Dimensions: Minimum 200x200px (validated client-side)
- Formats: JPEG, PNG, WebP only (enforced by bucket MIME types)
- Focus trap for crop modal (optional accessibility improvement)
- Escape key to close modal
- Retry mechanism with exponential backoff (already implemented in
uploadWithRetry()but not yet used in UI) - Image quality warnings for very small/large images