- Introduction
- Pitch
- Hogwarts
- Live Demo
- MVP
- Roadmap
- Launch Sprint
- PRD
- Get Started
- Localhost
- Architecture
- Structure
- Pattern
- Page
- Layout
- Content
- Types
- Config
- Actions
- Queries
- Authorization
- Validation
- Form
- Table
- Detail
- Card
- Util
- Hooks
- List Params
- Views
- README.md
- ISSUE.md
- Technology Stack
- Database
- File
- CDN Assets
- Entry Points
- Dashboard
- Authentication
- Credentials
- OAuth
- Flow Diagrams
- Multi-Tenancy
- Offline
- Onboarding
- Onboarding Videos
- Add Values
- Admission
- Application
- Attendance
- Compliance
- Profile
- Exams
- Exam Wizard
- Timetable
- Classrooms
- Notifications
- Conference
- LMS (Lumos)
Finance
- Finance
- Fee Management
- Invoice
- Wallet
- Salary
- Payroll
- Timesheet
- Expenses
- Budget
- Receipt
- Accounts
- Banking
- Reports
- Dashboard
- Permissions
- Messages
- Integration Flow
- Provision
- AI Document Processing
- Document Intelligence
- Internationalization
- Translation
- Translation Guide
- Icons
- Docs Factory
- Inspiration
- Listings
- Teachers
- Students
- Catalog
- Library
- Contributing
- Code of conduct
- GitHub Workflow
- Database Seeds
- Database Safety
- Test Accounts
- Playwright
- Prettier
- Block Rebound
Sales & GTM
validation.ts is the single source of truth for input shapes. Server actions and forms import from here — schemas don't live anywhere else.
Categories
| Category | Shape | Reference |
|---|---|---|
| CRUD entity | createSchema + updateSchema (.partial().extend({ id })) + filterSchema | library/validation.ts |
| Multi-step form | One schema per step + combined wizard schema with .refine() | onboarding/validation.ts |
| Search and filters | nuqs + Zod for URL query state | listings/students/list-params.ts |
| Auth | Login, register, reset, 2FA — often i18n factories | auth/login/validation.ts |
| Settings | Partial updates of a config record | school-dashboard/settings/validation.ts |
| Domain | Domain-specific shapes (timetable conflict, exam answer) | timetable/validation.ts |
Rules
| # | Rule |
|---|---|
| 1 | Co-export schema + inferred type — xSchema and type XInput = z.infer<typeof xSchema> |
| 2 | Named exports only |
| 3 | Use shared primitives from src/lib/validation/primitives.ts |
| 4 | Compose with .extend(), .partial(), .pick() |
| 5 | .refine() for cross-field validation |
| 6 | i18n factory pattern when error messages are user-facing |
| 7 | Stay under ~300 lines — split by concept |
Canonical example
import { z } from "zod"
export const subjectCreateSchema = z.object({
subjectName: z.string().min(1, "Subject name is required").max(100),
departmentId: z.string().cuid(),
description: z.string().max(500).optional(),
active: z.boolean().default(true),
})
export type SubjectCreateInput = z.infer<typeof subjectCreateSchema>
export const subjectUpdateSchema = subjectCreateSchema.partial().extend({
id: z.string().cuid(),
})
export type SubjectUpdateInput = z.infer<typeof subjectUpdateSchema>
export const subjectFilterSchema = z.object({
q: z.string().optional(),
departmentId: z.string().cuid().optional(),
active: z.boolean().optional(),
})
export type SubjectFilter = z.infer<typeof subjectFilterSchema>i18n factory
For translated error messages, accept the dictionary slice and return the schema:
export function createTitleSchema(d: Dictionary["onboarding"]["title"]) {
return z.object({
name: z.string().min(1, d.errors.required).max(100, d.errors.tooLong),
})
}
export type TitleInput = z.infer<ReturnType<typeof createTitleSchema>>Shared primitives
Lifted to src/lib/validation/primitives.ts:
export const emailSchema = z.string().email()
export const phoneSchema = z
.string()
.regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone")
export const slugSchema = z
.string()
.regex(/^[a-z0-9-]+$/, "Lowercase letters, numbers, hyphens only")
export const colorSchema = z
.string()
.regex(/^#[0-9a-f]{6}$/i, "Hex color (#RRGGBB)")
export const nameSchema = z.string().min(1).max(100)Anti-patterns
- Inline schemas in
actions.ts(14 files) — move tovalidation.ts. - Inline schemas in
form.tsx(15 files) — same fix. z.any()/z.unknown()(20 occurrences) — replace with the actual shape.- Bare
.min(1)without an error message (149 occurrences). - Duplicate primitives — 6 email regexes, 8 phone patterns. Lift to
primitives.ts. - Duplicate schemas across features — extract to
src/components/<shared>/validation.ts. - Bloated files —
onboarding/validation.ts(659),letters/validation.ts(546). Split.
Naming
- File:
validation.ts. - Suffixes:
<Name>Inputfor create/update,<Name>Filterfor filter shapes. Don't mixFormData,SchemaType,Schema.
Sibling roles
actions.tsimports<x>Schemaand validates withsafeParse.form.tsximports<x>Schema+<x>InputforuseForm({ resolver: zodResolver }).types.tsimports<x>Inputfor non-form references.queries.tsrarely imports — operates on Prisma types.