- 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
actions.ts runs on the server and mutates state. Every action follows the 5-step flow: authenticate → tenant → permission → validate → execute + revalidate. Reads belong in queries.ts.
Categories
| Category | Shape | Reference |
|---|---|---|
| CRUD | Create / read / update / delete (200–1,300 lines) | listings/grades/actions.ts |
| Form step | Validate one wizard step, no DB write | apply/personal/actions.ts |
| Integration | External API wrappers (Stripe, AI, email) | billing/actions.ts |
| SaaS admin | DEVELOPER-only platform operations | saas-dashboard/tenants/actions.ts |
| Auth | Pre-tenant; uses singular action.ts | auth/login/action.ts |
| File / upload | Signed URL minting, S3 confirms | file/upload/actions.ts |
Rules
| # | Rule |
|---|---|
| 1 | "use server" at the top |
| 2 | The 5-step flow — auth → tenant → permission → validate → execute + revalidate |
| 3 | Import ActionResponse from @/lib/action-response, never redefine |
| 4 | Validate with Zod schemas from validation.ts — never raw FormData |
| 5 | Mutations only — reads in queries.ts |
| 6 | try/catch with structured errors — return errorCode, never throw |
| 7 | Include schoolId in every query |
| 8 | Stay under ~300 lines — split into actions/ subdirectory by capability |
Canonical example — the 5 steps
"use server"
import { revalidatePath } from "next/cache"
import { auth } from "@/auth"
import type { ActionResponse } from "@/lib/action-response"
import { db } from "@/lib/db"
import { getTenantContext } from "@/lib/tenant-context"
import { canCreate } from "./authorization"
import { subjectCreateSchema, type SubjectCreateInput } from "./validation"
export async function createSubject(
input: SubjectCreateInput
): Promise<ActionResponse<{ id: string }>> {
try {
// 1. authenticate
const session = await auth()
if (!session?.user?.id) return { success: false, errorCode: "UNAUTHORIZED" }
// 2. tenant
const { schoolId } = await getTenantContext()
if (!schoolId) return { success: false, errorCode: "MISSING_TENANT" }
// 3. permission
if (!canCreate(session.user.role)) {
return { success: false, errorCode: "FORBIDDEN" }
}
// 4. validate
const parsed = subjectCreateSchema.safeParse(input)
if (!parsed.success) {
return { success: false, errorCode: "INVALID_INPUT" }
}
// 5. execute and revalidate
const subject = await db.subject.create({
data: { ...parsed.data, schoolId },
select: { id: true },
})
revalidatePath("/subjects")
return { success: true, data: { id: subject.id } }
} catch (error) {
console.error("createSubject error", error)
return { success: false, errorCode: "SERVER_ERROR" }
}
}Error codes
Return codes; the dictionary maps them on the client via useI18nMessages(dictionary).error.
| Code | Meaning |
|---|---|
UNAUTHORIZED | auth() failed |
MISSING_TENANT | No schoolId on the session |
FORBIDDEN | Permission check failed |
INVALID_INPUT | Zod validation failed |
NOT_FOUND | Record doesn't exist or other tenant |
CONFLICT | Unique constraint violation |
SERVER_ERROR | Unexpected exception |
type ActionResponse<T> =
| { success: true; data: T }
| { success: false; errorCode: ErrorCode; error?: string }The error field is for developer diagnostics only — never show to users.
Anti-patterns
- Redefining
ActionResponse(27+ files) — import from@/lib/action-response. - Monolith files — 5,692-line
timetable/actions.tsis the canonical bad example. Split intoactions/. - Duplicate function names across files (
createPeriod,getInvoices). console.logof auth data — never log emails, tokens, sessions.- Raw
FormData.get() as string— parse with Zod first. anytypes (150+ in codebase) — use inferred Zod or Prisma payloads.- Read functions in
actions.ts— move toqueries.ts.
Naming
- File:
actions.ts(plural).action.ts(singular) only for legacy auth flows.
Sibling roles
- Imports schemas + types from
validation.ts. - Imports permission helpers from
authorization.ts. - Never imports from
form.tsx/table.tsx. - Never imports from another feature's actions.