This is a multi-tenant SaaS platform for futsal field management. Each tenant is a business owner who manages one or more futsal venues — handling bookings, schedules, pricing, members, and payments from a single dashboard.
- Framework: Next.js 14 (App Router)
- API Layer: tRPC v11 (end-to-end typesafe)
- Database: Supabase (PostgreSQL) + Prisma ORM
- Auth: Supabase Auth (email + OAuth)
- Styling: Tailwind CSS + shadcn/ui
- Language: TypeScript (strict mode, no
any) - Package Manager: pnpm
- Multi-tenant: single database, tenant-scoped queries via
tenantIdforeign key - App Router with route groups:
(public),(auth),(dashboard) - tRPC routers split by domain:
tenant,field,booking,member,payment - Prisma as single source of truth for schema — all DB changes go through migrations
src/
├── app/ # Next.js App Router pages & layouts
│ ├── (public)/ # Landing, registration, public booking
│ ├── (auth)/ # Login, signup, forgot password
│ └── (dashboard)/ # Tenant dashboard (protected)
├── server/
│ ├── routers/ # tRPC routers (one per domain)
│ ├── trpc.ts # tRPC init, context, middleware
│ └── db.ts # Prisma client singleton
├── components/
│ ├── ui/ # shadcn/ui primitives (do NOT edit)
│ └── [domain]/ # Domain-specific components
├── lib/ # Shared utilities, constants, types
└── prisma/
├── schema.prisma # Single schema file
└── migrations/ # Prisma migrations (auto-generated)
These rules are non-negotiable. Follow them in every task, no exceptions.
- Never modify existing tests unless the ticket explicitly asks for it
- Never install new packages without asking first — suggest the package and wait for approval
- If requirements are unclear, ask before implementing — do not assume or guess intent
- Never edit
components/ui/— these are shadcn/ui primitives managed via CLI - Never push directly to
main— always work on a feature branch - Never skip migrations — all schema changes must go through
prisma migrate dev - Always scope queries by
tenantId— unscoped queries are security vulnerabilities - No
anytypes — useunknown+ type narrowing if the type is genuinely uncertain - Do not combine unrelated changes in the same commit or PR
- Read the full ticket before writing any code — including comments from previous tickets
- All code in TypeScript strict mode — no
ascasts unless absolutely necessary (add a// SAFETY:comment explaining why) - Prefer
constoverlet, never usevar - Use early returns to avoid deep nesting
- Max function length ~50 lines — extract helpers if longer
- File names:
kebab-case.tsfor utilities,PascalCase.tsxfor components
- Variables/functions:
camelCase - Types/interfaces:
PascalCase(noIprefix) - Constants:
UPPER_SNAKE_CASE - Enums:
PascalCaseenum name,UPPER_SNAKE_CASEvalues - Database columns:
camelCasein Prisma schema (maps tosnake_casein SQL via@map) - tRPC routers: named by domain —
tenantRouter,bookingRouter, etc. - tRPC procedures:
verb+Noun—createBooking,getField,listMembers
- Use path aliases:
@/server/...,@/components/...,@/lib/... - Group imports in order: (1) external packages, (2)
@/server, (3)@/components, (4)@/lib, (5) relative imports - No barrel exports (
index.tsre-exports) — import directly from the source file
- tRPC procedures: throw
TRPCErrorwith appropriate code (NOT_FOUND,FORBIDDEN,BAD_REQUEST, etc.) - Never swallow errors silently — always log or re-throw
- Use Zod for all input validation on tRPC procedures — no manual parsing
- Single
schema.prismafile — do not split into multiple files - Every model MUST have:
id— UUID,@default(uuid())createdAt—DateTime @default(now())updatedAt—DateTime @updatedAt
- Tenant-scoped models MUST have
tenantId Stringwith@relationtoTenant - Use
@map("snake_case")for column names,@@map("snake_case_plural")for table names - Enum values in
UPPER_SNAKE_CASE
- Always run
prisma migrate dev --name descriptive-nameafter schema changes - Never manually edit migration SQL files unless fixing a known issue
- Migration names:
kebab-casedescriptive — e.g.add-booking-status-enum,create-payment-table
- Always filter by
tenantId— use a middleware or helper that injects it automatically - Use
selectorincludeexplicitly — never fetch entire models when you only need a few fields - Paginate list queries — default page size 20, max 100
- Use transactions (
prisma.$transaction) for multi-step writes
- Every ticket with tRPC procedures includes an "API Tests" section with curl commands
- Run ALL listed test cases against the running dev server (
pnpm dev) - Post results as a Linear comment with ✅/❌ per test case before marking the ticket as Done
- Happy path: valid input → expected output shape and data
- Validation: invalid/missing fields →
BAD_REQUESTerror with descriptive message - Auth: unauthenticated requests →
UNAUTHORIZED - Tenant isolation: accessing another tenant's data →
FORBIDDENorNOT_FOUND - Edge cases: duplicate entries, empty lists, boundary values
- Test files live next to the source:
booking-router.ts→booking-router.test.ts - Use descriptive test names:
"should return 400 when booking date is in the past" - One assertion per test when possible — separate the "what" being tested
- Mock external services (Supabase Auth, payments) — never hit real APIs in tests
- Use shadcn/ui as the base — import from
@/components/ui/ - Domain components go in
@/components/[domain]/— e.g.@/components/booking/BookingCard.tsx - Page-level components go in the route's
_components/folder if only used in that page - Always type props with an explicit interface — no inline
{ }types in function params
- Tailwind utility classes only — no custom CSS files unless absolutely necessary
- Use
cn()helper (from@/lib/utils) for conditional class merging - Follow shadcn/ui color tokens —
primary,secondary,muted,destructive, etc. - Responsive: mobile-first — start with base styles, add
sm:,md:,lg:breakpoints - No hardcoded colors — always use Tailwind theme tokens
- Use
react-hook-form+zodresolver for all forms - Share Zod schemas between tRPC input validation and form validation when possible
- Always show validation errors inline below the field
- Disable submit button while submitting, show loading state
- Use tRPC's React Query hooks —
trpc.booking.list.useQuery(), etc. - Show loading skeletons (not spinners) for initial data loads
- Show inline error states with retry option on failure
- Optimistic updates for quick actions (toggle, delete) — rollback on error
- Pull the ticket from Linear: use
mcp__linear-server__get_issuewith the issue ID - Read comments on the immediately preceding ticket (e.g. if starting E3-003, read E3-002 comments) using
mcp__linear-server__list_comments— look for handoff notes left by the previous agent - Read the full description, acceptance criteria, technical notes, and API tests
- Set the ticket status to In Progress via
mcp__linear-server__save_issue
- Always create a new branch before starting any implementation
- Branch name format:
{ticket-id}/{short-description}- Example:
ifi-15/tenant-schema,ifi-16/tenant-registration-form
- Example:
- If the current ticket depends on a previous ticket that is not yet merged, branch off that ticket's branch instead of
main- Check the ticket's "Blocked by" field in Linear to determine the parent branch
- Otherwise always branch from
main
After implementation is verified (build passes, API tests pass):
- Use atomic commits: group logically related files into one commit — do not commit file-by-file, but also do not dump all changes in a single commit if they span unrelated concerns
- Follow Conventional Commits format:
feat(scope): description— new feature or endpointfix(scope): description— bug fixrefactor(scope): description— code restructure without behavior changechore(scope): description— tooling, deps, configdocs(scope): description— documentation only- Scope is optional but preferred (e.g.
feat(tenant): add registration form)
- Keep commit messages concise — describe what changed and why, not how
- Example groupings for a schema + API + UI ticket:
- Commit 1:
feat(schema): add Tenant and TenantStatusLog models with migration - Commit 2:
feat(tenant): add tenant registration tRPC procedure - Commit 3:
feat(ui): add public tenant registration form
- Commit 1:
- Add implementation notes as comments via
mcp__linear-server__save_comment - If a blocker is discovered, add a comment with the blocker details and leave status as In Progress
- For every ticket that introduces tRPC procedures, run ALL curl test cases specified in the ticket's "API Tests" section
- Test the running dev server (
pnpm dev) — do not skip tests - Post test results (pass/fail per case) as a comment on the ticket before marking Done
-
Run all API tests listed in the ticket and post results as a comment
-
Post a completion comment using this structure:
What was built
- {Bullet list of features/endpoints/UI pages implemented}
Key files changed
{file path}— {what changed}
API test results
- ✅ {test case}: passed — {actual response summary}
- ❌ {test case}: failed — {error}
Handoff notes
- {Context the next ticket needs: schema fields added, helpers introduced, patterns to follow}
- {Any deviations from the original spec and why}
Formatting rule: wrap all code references in backticks — enum values, field names, model names, function names, status constants, file paths, and CLI commands.
-
Set status to In Review (needs human review) or Done (self-contained + tests pass)
-
Create atomic commits following Conventional Commits (see "Committing" section)
-
Push the branch and open a GitHub PR to
main(see "Creating a PR" section) -
Post the PR URL as a follow-up comment on the Linear ticket
- Work in epic dependency order: E1 → E2 → E3/E4/E7 (parallel) → E5 → E6
- Within an epic, work tickets in numeric order unless a specific one is unblocked earlier
- Use
mcp__linear-server__list_issuesfiltered by project + status=Todo to find next work - Pull the full ticket with
mcp__linear-server__get_issuebefore starting — never work from memory alone
| Status | Meaning |
|---|---|
| Backlog | Not yet scheduled |
| Todo | Scheduled for current session |
| In Progress | Actively being worked on |
| In Review | Built, awaiting review / testing |
| Done | Verified complete — all AC checked, API tests passed |
| Canceled | Descoped or not needed |
After all commits are pushed, open a GitHub PR targeting main:
Title format: [{TICKET-ID}] {ticket title}
- Example:
[IFI-15] Database Schema — Core Tables (Tenants, Users, Sessions)
PR description format:
## Overview
{1–2 sentences describing what this PR does and why}
- {Bullet expanding on a key detail, decision, or scope point}
- {Another notable aspect — e.g. what was removed, replaced, or why an approach was chosen}
## Changes
- `{file or area}` — {what changed}
- ...
## Testing
- {Step-by-step instructions to verify the changes work}
- {Include any seed data, env vars, or setup needed}
## Notes
{Optional paragraph for deviations from spec, caveats, or anything the reviewer should know.
Omit this section if there is nothing notable.}
## Linear
{Linear ticket URL}
- After the PR is created, post its URL as a separate comment on the Linear ticket
- Use
mcp__linear-server__save_commentwith the PR URL and a one-line summary