This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
OpenLeague is a free, open-source sports team management platform built with Next.js 16, React 19, and TypeScript. The application uses a modern stack with MUI v7 (with Emotion), Tailwind CSS v4, Prisma 7 with PostgreSQL (Neon), and Auth.js for authentication. The project follows a mobile-first design philosophy and uses Next.js Server Actions as the primary data mutation pattern.
# Development
bun run dev # Start dev server with Turbopack at localhost:3000
bun run dev:wake # Wake database then start dev (for Neon serverless)
bun run build # Production build
bun run start # Start production server
bun run type-check # TypeScript type checking (run before commits)
bun run lint # ESLint
# Testing
bun run test # Run tests with Vitest
bun run test:watch # Run tests in watch mode
bun run test:coverage # Generate coverage report
bun run test:ui # Open Vitest UI
# Database Operations
bun run db:studio # Open Prisma Studio (visual database browser)
bun run db:migrate # Create and apply migration (development)
bun run db:migrate:deploy # Deploy migrations (production only)
bun run db:migrate:reset # Reset database - DESTRUCTIVE (dev only)
bun run db:generate # Generate Prisma Client (run after schema changes)
bun run db:push # Push schema changes without migration (dev prototyping)
bun run db:seed # Run seed script
bun run db:wake # Wake up Neon database (serverless)
# Utilities
bun run validate-env # Validate environment variablesImportant: Always use bun (not npm or yarn) for package management and running scripts.
# Run a specific test file
bun run test __tests__/lib/utils/validation.test.ts
# Run tests matching a pattern
bun run test --grep "event validation"
# Watch a specific test file
bun run test:watch __tests__/lib/utils/validation.test.ts- Framework: Next.js 16 App Router (NOT Pages Router) with React 19
- Data Mutations: Server Actions first, API routes only for webhooks/external integrations
- Data Fetching: React Server Components by default, Client Components only when needed
- Styling: MUI v7 with Emotion + Tailwind CSS v4 (MUI is primary component library; Tailwind for utility styling)
- Database: Prisma 7 ORM with PostgreSQL - parameterized queries prevent SQL injection; config in
prisma/prisma.config.ts - Validation: Zod v4 (API differs from v3 — check schemas carefully)
- Authentication: Auth.js v5 with credential provider, bcrypt password hashing
- Email: Mailchimp Transactional Email (abstracted for future AWS SES migration)
app/ # Next.js App Router
├── (auth)/ # Public auth routes (login, signup)
├── (marketing)/ # Public marketing pages (about, pricing, etc.)
├── (dashboard)/ # Protected routes requiring authentication
│ ├── layout.tsx # Dashboard layout with navigation
│ ├── page.tsx # Team dashboard (default view)
│ ├── roster/ # Roster management
│ ├── calendar/ # Event calendar views
│ ├── events/ # Event creation and details
│ ├── schedules/ # Game schedule management
│ ├── venues/ # Venue CRUD
│ ├── league/ # League management (if applicable)
│ ├── practice-planner/ # Practice session planning with rink board
│ ├── dashboard/ # Main dashboard view
│ └── admin/ # Admin-only features
├── api/ # API routes (minimal - prefer Server Actions)
│ ├── auth/[...nextauth]/ # Auth.js endpoints
│ ├── cron/ # Scheduled jobs (RSVP reminders, notification batches)
│ ├── invitations/ # Invitation acceptance webhooks
│ ├── leagues/ # League API (team listing)
│ └── roster/export/ # CSV roster export (GET endpoint — file download, not a mutation)
└── docs/ # Documentation pages
components/ # React components
├── features/ # Feature-specific components (grouped by domain)
│ ├── roster/ # RosterList, PlayerCard, AddPlayerDialog, TeamOfficialCard
│ ├── dashboard/ # DashboardNav, DashboardSidebar
│ ├── events/ # EventForm, EventCard, RSVPButton
│ ├── practice-planner/ # RinkBoard, DrawingToolbar, PlayEditor, PlayLibrary
│ └── navigation/ # MobileNavigation, Breadcrumbs
├── ui/ # Generic reusable UI primitives
└── providers/ # Context providers (LeagueProvider, ThemeProvider, etc.)
lib/ # Core application logic
├── actions/ # Server Actions (primary mutation method)
│ ├── auth.ts # Signup, login (no raw queries, uses Prisma)
│ ├── team.ts # Team CRUD
│ ├── roster.ts # Player management
│ ├── events.ts # Event scheduling
│ ├── rsvp.ts # Attendance tracking
│ ├── invitations.ts # Email invitations
│ ├── league.ts # League management
│ ├── league-context.ts # League context resolution helpers
│ ├── team-context.ts # Team context resolution helpers
│ ├── communication.ts # Messaging system
│ ├── notifications.ts # Notification preferences
│ ├── permissions.ts # Permission checks
│ ├── admin.ts # Admin operations
│ ├── audit.ts # Audit logging
│ ├── logout.ts # Logout action
│ ├── plays.ts # Play/drill management (practice planner)
│ ├── practice-sessions.ts # Practice session management
│ ├── practice-session-queries.ts # Read-only practice session queries
│ ├── game-schedules.ts # Game schedule CRUD
│ └── venues.ts # Venue management
├── auth/ # Authentication utilities
│ ├── config.ts # Auth.js configuration
│ └── session.ts # Session helpers (requireAuth, requireTeamAdmin, etc.)
├── db/
│ └── prisma.ts # Prisma Client singleton
├── email/ # Email service abstraction
│ ├── client.ts # Mailchimp client
│ └── templates.ts # Email templates
├── utils/ # Shared utilities
│ ├── validation.ts # Zod schemas
│ ├── date.ts # Date formatting
│ ├── permissions.ts # Permission utilities
│ ├── security.ts # Security utilities
│ ├── sanitization.ts # Input sanitization helpers
│ ├── rate-limit.ts # Rate limiting
│ ├── error-handling.ts # Error utilities
│ ├── csv.ts # CSV generation primitives
│ ├── csv-export.ts # Roster/data export helpers
│ ├── league-mode.ts # League vs standalone team mode detection
│ └── data-migration.ts # Data migration utilities
└── hooks/ # React hooks for Client Components
types/ # Shared TypeScript types
├── roster.ts # Player, TeamOfficial, export types
├── events.ts # Event and RSVP types
├── practice-planner.ts # Practice session and play types
├── auth.ts # Session and auth types
└── invitations.ts # Invitation types
specs/ # SpecKit feature specifications
└── <feature-name>/ # One folder per feature
├── spec.md # Feature specification
├── plan.md # Implementation plan
├── tasks.md # Task breakdown
└── ...
prisma/
├── schema.prisma # Database schema (single source of truth)
└── migrations/ # Migration history
The Prisma schema (prisma/schema.prisma) defines the complete data model:
Core Models:
User- Authentication and user profilesTeam- Team information (can be standalone or part of a league)TeamMember- Junction table linking users to teams with roles (ADMIN/MEMBER)Player- Roster entries (may or may not have User accounts)Event- Games and practices with schedulingRSVP- Attendance responses (GOING, NOT_GOING, MAYBE, NO_RESPONSE)Invitation- Email invitations with tokens and expiration
League Models (optional - teams can exist without leagues):
League- Multi-team organizationDivision- Grouping within leagues (age groups, skill levels)LeagueUser- League membership with roles (LEAGUE_ADMIN, TEAM_ADMIN, MEMBER)PlayerTransfer- Audit trail for player moves between teams
Communication Models:
LeagueMessage- Targeted messages to divisions/teams/entire leagueMessageTargeting- Defines message recipientsMessageRecipient- Delivery trackingNotificationPreference- User notification settingsNotificationBatch- Batched message deliveryBatchedMessage- Individual messages in a batch
Audit Model:
AuditLog- Administrative actions and security events
Key Schema Patterns:
- Optional league relationships (
leagueId?) allow teams to operate standalone or within leagues - Indexes on frequently queried fields (userId, teamId, leagueId, date ranges)
- Cascading deletes (
onDelete: Cascade) for proper cleanup @uniqueconstraints enforce data integrity
Pattern: Every Server Action must validate authentication first
// Standard auth pattern in Server Actions
const userId = await requireUserId(); // Throws and redirects if not authenticatedKey Auth Helpers (lib/auth/session.ts):
requireAuth()- Redirects to login if not authenticated, returns sessionrequireUserId()- Returns userId or redirects to login
Auth Error Handling: Login/signup pages display user-friendly error messages (not raw exceptions). Auth Server Actions return structured ActionResult with descriptive error strings for the UI.
requireTeamAdmin(teamId)- Ensures user is ADMIN role for teamrequireTeamMember(teamId)- Ensures user belongs to teamrequireLeagueRole(leagueId, role)- Ensures user has required league roleisSystemAdmin(userId)- Check if user is league admin
Role Hierarchy:
- Team Level:
ADMIN(full team control) andMEMBER(view + RSVP) - League Level:
LEAGUE_ADMIN(full league control),TEAM_ADMIN(manage own team),MEMBER(basic access)
Server Actions are the primary method for data mutations (not API routes). They follow a consistent pattern:
"use server";
import { z } from "zod";
import { prisma } from "@/lib/db/prisma";
import { requireUserId } from "@/lib/auth/session";
import { revalidatePath } from "next/cache";
export type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string; details?: unknown };
export async function myAction(input: InputType): Promise<ActionResult<OutputType>> {
try {
// 1. Always authenticate first
const userId = await requireUserId();
// 2. Validate input with Zod
const validated = mySchema.parse(input);
// 3. Check authorization (team admin, etc.)
await requireTeamAdmin(validated.teamId);
// 4. Perform database operation
const result = await prisma.model.create({
data: validated,
});
// 5. Revalidate affected pages
revalidatePath("/path");
return { success: true, data: result };
} catch (error) {
return { success: false, error: "Friendly error message" };
}
}Critical Security Rules:
- NEVER trust client input - always validate with Zod
- ALWAYS check authentication first (
requireUserId()) - ALWAYS verify authorization (admin role, team membership)
- ALWAYS use Prisma (parameterized queries) - NEVER raw SQL
- ALWAYS sanitize user input displayed in UI
- NEVER expose sensitive data (emergency contacts to non-admins)
Default: React Server Components
// app/(dashboard)/roster/page.tsx
export default async function RosterPage({ params }: { params: { teamId: string } }) {
// Fetch directly in Server Component
const players = await prisma.player.findMany({
where: { teamId: params.teamId },
});
return <RosterList players={players} />;
}Only use Client Components when you need:
- User interactions (forms, buttons)
- Browser APIs (localStorage, window)
- React hooks (useState, useEffect)
- Optimistic updates (useOptimistic for RSVP buttons)
Comprehensive security measures (see docs/SECURITY_IMPLEMENTATION.md for details):
- Authentication: Auth.js with bcrypt password hashing (cost factor 12)
- Authorization: Role-based access control checked in every Server Action
- Input Validation: Zod schemas on all user inputs (client and server)
- SQL Injection Prevention: Prisma ORM with parameterized queries (no raw SQL)
- XSS Prevention: React's built-in escaping + CSP headers
- CSRF Protection: Auth.js built-in token validation
- HTTPS Enforcement: Proxy (
proxy.ts) redirects HTTP to HTTPS in production - Rate Limiting: Applied to API routes (auth: 5 req/15min, general: 100 req/15min)
- Security Headers: Configured in
next.config.ts(HSTS, CSP, X-Frame-Options, etc.) - Password Security: Minimum length 8 chars, bcrypt hashing
- Session Management: HTTP-only cookies, secure JWT tokens
- Data Sanitization: Input sanitization for user-generated content
Critical Security Headers (next.config.ts):
Strict-Transport-Security: HTTPS enforcementContent-Security-Policy: Prevents XSS attacksX-Frame-Options: SAMEORIGIN: Prevents clickjackingX-Content-Type-Options: nosniff: MIME type sniffing protection
Responsive Breakpoints (MUI theme):
xs: <600px (mobile)sm: 600-960px (tablet)md: >960px (desktop)
Key Mobile Patterns:
- Bottom navigation on mobile, sidebar on desktop
- Calendar: grid view on desktop, list view on mobile
- Touch targets: minimum 44x44px
- Forms optimized for mobile input (proper keyboard types)
- Tables convert to card layouts on mobile
Email service is abstracted (lib/email/) for future AWS SES migration:
- Current: Mailchimp Transactional Email
- Future: Easy migration to AWS SES by updating
client.ts
Email Templates (lib/email/templates.ts):
- Team invitations with signup links
- Event notifications (created/updated/cancelled)
- RSVP reminders (48 hours before events)
- Welcome emails for new users
- League announcements
- Targeted messages by division/team
Cron Jobs (app/api/cron/):
- RSVP reminders: Runs hourly, sends reminders 48 hours before events
Required Environment Variables (validate with bun run validate-env):
DATABASE_URL # PostgreSQL connection string (must include ?sslmode=require for Neon)
NEXTAUTH_URL # Application URL (http://localhost:3000 for dev)
NEXTAUTH_SECRET # Generate with: openssl rand -base64 32
MAILCHIMP_API_KEY # Mailchimp Transactional API key
EMAIL_FROM # Verified sender email addressOptional Variables:
NEXT_PUBLIC_UMAMI_WEBSITE_ID # Umami analytics (privacy-friendly)
AWS_REGION # For future AWS migrationWorkflow for schema modifications:
# 1. Edit prisma/schema.prisma
# 2. Create and apply migration
bun run db:migrate
# 3. Generate updated Prisma Client (updates TypeScript types)
bun run db:generate
# 4. Test changes
bun run devMigration commands:
- Development:
bun run db:migrate(creates migration files and applies them) - Production:
bun run db:migrate:deploy(applies existing migrations) - Prototyping:
bun run db:push(skips migration files, direct schema push)
Important: Always commit both schema.prisma and migration files together.
Standard feature implementation flow:
- Define validation schema (
lib/utils/validation.ts):
export const myFeatureSchema = z.object({
field: z.string().min(1, "Required"),
});
export type MyFeatureInput = z.infer<typeof myFeatureSchema>;- Create Server Action (
lib/actions/my-feature.ts):
"use server";
export async function myFeatureAction(input: MyFeatureInput) {
const userId = await requireUserId();
const validated = myFeatureSchema.parse(input);
// ... implementation
}- Build UI component (
components/features/my-feature/):
"use client"; // Only if user interaction needed
export function MyFeatureForm() {
// Form with client-side validation using Zod schema
}- Create page (
app/(dashboard)/my-feature/page.tsx):
// Server Component for data fetching
export default async function MyFeaturePage() {
const data = await prisma.model.findMany();
return <MyFeatureForm data={data} />;
}Test structure:
import { describe, it, expect } from 'vitest';
describe('Feature Name', () => {
it('should handle valid input', () => {
// Test implementation
});
it('should reject invalid input', () => {
// Test validation
});
});Key testing areas:
- Zod validation schemas
- Server Action logic (mock Prisma)
- React components (Testing Library)
- Integration tests for critical flows
Pre-existing test failures (not regressions — do not attempt to fix):
theme-marketing.test.ts— marketing theme variant testsDragDropTeams.test.tsx— DnD context issues
- Always use Server Actions, not API routes - API routes only for webhooks/cron
- Never forget
"use server"directive - Required at top of Server Action files - Never trust client input - Always validate with Zod on server
- Prisma Client must be regenerated - After schema changes, run
bun run db:generate - Revalidate paths after mutations - Use
revalidatePath()in Server Actions - Emergency contacts are admin-only - Never expose to regular members
- Player vs User distinction - Player is roster entry, User is authenticated account
- League relationships are optional - Teams can exist standalone (leagueId is nullable)
- Zod v4 is in use - API differs from Zod v3 (e.g.,
z.string().min()still works, but some advanced patterns changed) proxy.tsnotmiddleware.ts- Next.js 16 renamed middleware to proxy; runs on Node.js runtime
Proxy (proxy.ts, Next.js 16 replacement for middleware.ts) handles:
- HTTPS enforcement in production
- Rate limiting for API routes (auth: 5 req/15min, general: 100 req/15min)
- Security header injection (X-Robots-Tag)
- Applied to all routes except static files (
_next/static,_next/image,favicon.ico)
Automatic on Vercel (configured in vercel.json):
- Build command:
bun run build - Install command:
bun install - Prisma Client generation: Runs automatically via
postinstallscript (prisma generate) - Environment variables: Must be configured in Vercel dashboard
Pre-deployment checklist:
- All environment variables set in Vercel
- Database connection tested
- Email service configured (domain verified)
bun run type-checkpassesbun run lintpassesbun run testpasses
OpenLeague uses semantic versioning with conventional commits:
feat:commits trigger minor version bump (0.X.0)fix:commits trigger patch version bump (0.0.X)feat!:orBREAKING CHANGE:trigger major version bump (X.0.0)
Release process:
- Merge to
mainbranch triggers automated release workflow - GitHub Actions runs type-checking, linting, and tests
- Semantic version determined from commit messages
- Changelog generated automatically
- GitHub release created with assets
See .github/AUTOMATION.md for full CI/CD details.
Project Documentation:
README.md- Comprehensive setup and feature documentationSETUP.md- Development setup and implementation progressDEPLOYMENT.md- Detailed deployment guideSECURITY.md- Security policy and vulnerability reportingdocs/SECURITY_IMPLEMENTATION.md- Security measures and implementation details.github/CONTRIBUTING.md- Contribution guidelines.github/AUTOMATION.md- CI/CD and release automation
External Documentation:
- Next.js 16: https://nextjs.org/docs
- React 19: https://react.dev
- MUI v7: https://mui.com/material-ui/
- Prisma: https://www.prisma.io/docs
- Auth.js: https://authjs.dev/
- Zod: https://zod.dev/
Business Source License 1.1 (BUSL-1.1) - converts to Apache 2.0 on October 4, 2029. Commercial use as a service to third parties requires a commercial license. Self-hosting for your own organization is freely permitted.