Replace tracked symlinks with files

This commit is contained in:
2026-06-07 00:38:53 -06:00
parent 824eac7d8d
commit 6414cf8087
209 changed files with 20765 additions and 7 deletions
-1
View File
@@ -1 +0,0 @@
../agents/rules
+79
View File
@@ -0,0 +1,79 @@
# Cal.diy Engineering Rules
This directory contains modular, machine-readable engineering rules derived from [Cal.diy's Engineering Standards for 2026 and Beyond](https://cal.com/blog/engineering-in-2026-and-beyond).
## Structure
Rules are organized by section prefix, as defined in `_sections.md`:
| Prefix | Section | Impact |
|--------|---------|--------|
| `architecture-` | Architecture | CRITICAL |
| `quality-` | Code Quality | CRITICAL |
| `data-` | Data Layer | HIGH |
| `api-` | API Design | HIGH |
| `performance-` | Performance | HIGH |
| `testing-` | Testing | MEDIUM-HIGH |
| `patterns-` | Design Patterns | MEDIUM |
| `culture-` | Team Culture | MEDIUM |
## Files
- `_sections.md` - Defines all sections, their ordering, and impact levels
- `_template.md` - Template for creating new rules
- `{section}-{rule-name}.md` - Individual rule files
## Rule Format
Each rule file follows a consistent format with YAML frontmatter:
```markdown
---
title: Rule Title Here
impact: CRITICAL | HIGH | MEDIUM | LOW
impactDescription: Optional description (e.g., "20-50% improvement")
tags: tag1, tag2, tag3
---
## Rule Title Here
**Impact: LEVEL (optional description)**
Brief explanation of the rule and why it matters.
**Incorrect (description):**
\`\`\`typescript
// Bad code example
\`\`\`
**Correct (description):**
\`\`\`typescript
// Good code example
\`\`\`
Reference: [Link](url)
```
## Adding New Rules
1. Copy `_template.md` to a new file with the appropriate section prefix
2. Fill in the frontmatter (title, impact, tags)
3. Write a clear explanation of the rule
4. Provide incorrect and correct code examples
5. Add a reference link if applicable
## Usage
These rules are designed to be:
- **Human-readable**: Engineers can browse and learn from them
- **Machine-readable**: AI agents can parse and apply them
- **Modular**: Individual rules can be updated without affecting others
- **Versionable**: Changes are tracked in git history
## Core Principles
From the blog post, our engineering philosophy is:
> We are building infrastructure that must almost never fail. To achieve this, we move fast while shipping amazing quality software with no shortcuts or compromises.
The rules in this directory encode the specific practices that enable this philosophy.
+56
View File
@@ -0,0 +1,56 @@
# Sections
This file defines all sections, their ordering, impact levels, and descriptions.
The section ID (in parentheses) is the filename prefix used to group rules.
---
## 1. Architecture (architecture)
**Impact:** CRITICAL
**Description:** Vertical Slice Architecture and Domain-Driven Design patterns that form the foundation of our codebase organization.
## 2. Code Quality (quality)
**Impact:** CRITICAL
**Description:** Standards for maintaining high-quality, maintainable code including PR reviews, testing, and accountability.
## 3. Data Layer (data)
**Impact:** HIGH
**Description:** Repository patterns, DTOs, and technology isolation to prevent coupling and enable maintainability.
## 4. API Design (api)
**Impact:** HIGH
**Description:** Controller patterns, API stability, and proper separation of HTTP concerns from business logic.
## 5. Performance (performance)
**Impact:** HIGH
**Description:** Algorithm complexity, data structure choices, and performance patterns for enterprise scale.
## 6. Testing (testing)
**Impact:** MEDIUM-HIGH
**Description:** Test coverage requirements and testing strategies for maintaining code quality.
## 7. Design Patterns (patterns)
**Impact:** MEDIUM
**Description:** Factory patterns, dependency injection, and other design patterns for clean, maintainable code.
## 8. Team Culture (culture)
**Impact:** MEDIUM
**Description:** Engineering culture, accountability, and collaboration standards.
## 9. CI/CD (ci)
**Impact:** HIGH
**Description:** Continuous integration practices, type checking priorities, and git workflow standards.
## 10. Reference (reference)
**Impact:** LOW
**Description:** Informational lookups, file locations, and local development setup guides.
+28
View File
@@ -0,0 +1,28 @@
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description of impact (e.g., "20-50% improvement")
tags: tag1, tag2
---
## Rule Title Here
**Impact: MEDIUM (optional impact description)**
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the implications.
**Incorrect (description of what's wrong):**
```typescript
// Bad code example here
const bad = example()
```
**Correct (description of what's right):**
```typescript
// Good code example here
const good = example()
```
Reference: [Link to documentation or resource](https://example.com)
+60
View File
@@ -0,0 +1,60 @@
---
title: Never Introduce Breaking API Changes
impact: CRITICAL
impactDescription: Maintains developer trust and prevents integration nightmares
tags: api, stability, versioning, backwards-compatibility
---
## Never Introduce Breaking API Changes
**Impact: CRITICAL**
Once an API endpoint is public, it must remain stable. Breaking changes destroy developer trust and create integration nightmares for our users.
**Strategies for avoiding breaking changes:**
- Always add new fields as optional
- Use API versioning when you must change existing behavior
- Deprecate old endpoints gracefully with clear migration paths
- Maintain backward compatibility for at least two major versions
**Incorrect (breaking change):**
```typescript
// v1 - Original response
interface BookingResponse {
id: number;
startTime: string; // ISO string
}
// v1 - Breaking change: renamed field
interface BookingResponse {
id: number;
start: string; // Renamed from startTime - BREAKS CLIENTS
}
```
**Correct (non-breaking evolution):**
```typescript
// v1 - Original response
interface BookingResponse {
id: number;
startTime: string;
}
// v1 - Non-breaking: add new field, keep old one
interface BookingResponse {
id: number;
startTime: string; // Keep for backwards compatibility
start: string; // New preferred field
}
```
**When you must make breaking changes:**
- Create a new API version using date-specific versioning in API v2
- Run both versions simultaneously during transition
- Provide automated migration tools when possible
- Give users ample time to migrate (minimum 6 months for public APIs)
- Document exactly what changed and why
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
+73
View File
@@ -0,0 +1,73 @@
---
title: Keep Controllers Thin - HTTP Concerns Only
impact: HIGH
impactDescription: Enables technology-agnostic business logic
tags: api, controllers, http, separation-of-concerns
---
## Keep Controllers Thin - HTTP Concerns Only
**Impact: HIGH**
Controllers are thin layers that handle only HTTP concerns. They take requests, process them, and map data to DTOs that are passed to core application logic. No application or core logic should be seen in API routes or tRPC handlers.
**Controller responsibilities (and ONLY these):**
- Receive and validate incoming requests
- Extract data from request parameters, body, headers
- Transform request data into DTOs
- Call appropriate application services with those DTOs
- Transform application service responses into response DTOs
- Return HTTP responses with proper status codes
**Controllers should NOT:**
- Contain business logic or domain rules
- Directly access databases or external services
- Perform complex data transformations or calculations
- Make decisions about what the application should do
- Know about implementation details of the domain
**Incorrect (business logic in controller):**
```typescript
export async function POST(request: Request) {
const body = await request.json();
// Business logic in controller - BAD
const user = await prisma.user.findFirst({ where: { id: body.userId } });
if (!user.canBook) {
return Response.json({ error: "Cannot book" }, { status: 403 });
}
const booking = await prisma.booking.create({
data: {
title: body.title,
startTime: new Date(body.startTime),
// Complex logic here...
}
});
return Response.json(booking);
}
```
**Correct (thin controller):**
```typescript
export async function POST(request: Request) {
const body = await request.json();
// Validate input
const input = CreateBookingSchema.parse(body);
// Delegate to service
const result = await bookingService.createBooking(input);
// Transform and return
return Response.json(BookingResponseSchema.parse(result));
}
```
**The principle:**
We must detach HTTP technology from our application. The way we transfer data between client and server (whether REST, tRPC, etc.) should not influence how our core application works. HTTP is a delivery mechanism, not an architectural driver.
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
@@ -0,0 +1,193 @@
---
title: Prevent Circular Dependencies Between Packages
impact: CRITICAL
impactDescription: Prevents build failures, type errors, ensures honest implementation, and maintains clean dependency graph
tags: architecture, dependencies, packages, imports, circular
---
## Prevent Circular Dependencies Between Packages
**Impact: CRITICAL**
Circular dependencies between packages cause build failures, type errors, and make the codebase unmaintainable. The dependency graph must be acyclic, with clear layers from low-level utilities up to high-level features.
The dependency hierarchy (from lowest to highest):
```
packages/lib (lowest - no feature dependencies)
packages/app-store
packages/features
packages/trpc
apps/web (highest - can depend on all packages)
```
**Benefits:**
- **Honest implementation**: The dependency graph accurately reflects what code depends on what, with no hidden or circular relationships that create false expectations
- **Predictable behavior**: Code behaves as the dependency structure suggests - no surprises from circular imports
- **Clear mental model**: Developers can reason about the system without tracking complex circular relationships
- **Build reliability**: No circular dependency errors during compilation or bundling
- **Easier testing**: Lower-level packages can be tested independently without pulling in the entire codebase
### `packages/lib`
**Rules:**
1. No files in `packages/lib` import from `@calcom/app-store` or `../app-store/**`
2. No files in `packages/lib` import from `@calcom/features` or `../features/**`
3. No files in `packages/lib` import from `@calcom/trpc` or `../trpc/**`
4. No files in `packages/lib` import from `@trpc/server`
5. **BONUS:** No repository files allowed in `packages/lib` (repositories belong in features or app-store)
**Incorrect:**
```typescript
// Bad - lib importing from features
import { getBooking } from "@calcom/features/bookings";
import { EventRepository } from "@calcom/features/events/repositories";
// Bad - lib importing from app-store
import { googleCalendarHandler } from "@calcom/app-store/googlecalendar";
// Bad - lib importing from trpc
import { router } from "@calcom/trpc/server/trpc";
```
**Correct:**
```typescript
// Good - lib only imports from other lib files or external packages
import { logger } from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import type { User } from "@prisma/client";
```
### `packages/app-store`
**Rules:**
6. No files in `packages/app-store` import from `@calcom/features` or `../features/**`
7. No files in `packages/app-store` import from `@calcom/trpc` or `../trpc/**`
**Incorrect:**
```typescript
// Bad - app-store importing from features
import { BookingService } from "@calcom/features/bookings/services";
// Bad - app-store importing from trpc
import { publicProcedure } from "@calcom/trpc/server/trpc";
```
**Correct:**
```typescript
// Good - app-store imports from lib and prisma
import { logger } from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import type { Credential } from "@prisma/client";
```
### `packages/features`
**Rules:**
8. No files in `packages/features` import from `@calcom/trpc` or `../trpc/**`
9. No files in `packages/features` import from `@calcom/web` or `@calcom/web/**`
10. No files in `packages/features` import from `../../apps/web/**`
**Incorrect:**
```typescript
// Bad - features importing from trpc
import { router } from "@calcom/trpc/server/trpc";
// Bad - features importing from web app
import { getServerSession } from "@calcom/web/lib/auth";
import { WebComponent } from "../../apps/web/components/WebComponent";
```
**Correct:**
```typescript
// Good - features import from lib, app-store, and prisma
import { logger } from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import { googleCalendarHandler } from "@calcom/app-store/googlecalendar";
```
### `packages/trpc`
**Rules:**
11. No files in `packages/trpc` import from `../apps/web/**`
**Incorrect:**
```typescript
// Bad - trpc importing from web app
import { getServerSession } from "../../apps/web/lib/auth";
```
**Correct:**
```typescript
// Good - trpc imports from features, lib, and prisma
import { BookingService } from "@calcom/features/bookings/services";
import { logger } from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
```
### `packages/testing`
**Rules:**
12. No files in `packages/testing` import from `@calcom/web` or `@calcom/web/**`
13. No files in `packages/testing` import from `@calcom/features` or `@calcom/features/**`
14. No files in `packages/testing` import from `../../apps/web/**`
15. No files in `packages/testing` import from `../features/**`
**Incorrect:**
```typescript
// Bad - testing importing from web or features
import { WebComponent } from "@calcom/web/components";
import { BookingService } from "@calcom/features/bookings";
```
**Correct:**
```typescript
// Good - testing imports from lib and prisma
import { prisma } from "@calcom/prisma";
import { logger } from "@calcom/lib/logger";
```
### `packages/platform/atoms`
**Rules:**
16. No files in `packages/platform/atoms` import from `@calcom/trpc` or `@calcom/trpc/**`
17. No files in `packages/platform/atoms` import from `../../trpc` or `../../trpc/**`
18. No files in `packages/platform/atoms` import from `@calcom/web`
**Incorrect:**
```typescript
// Bad - platform/atoms importing from trpc or web
import { router } from "@calcom/trpc/server/trpc";
import { trpc } from "../../trpc";
import { WebComponent } from "@calcom/web";
```
**Correct:**
```typescript
// Good - platform/atoms imports from lib, features, and ui
import { logger } from "@calcom/lib/logger";
import { Button } from "@calcom/ui/components/button";
import type { Booking } from "@calcom/features/bookings/types";
```
## Enforcement
These rules should be enforced through:
- ESLint rules that detect forbidden import paths
- CI checks that fail on circular dependencies
- Code review guidelines that flag violations
@@ -0,0 +1,42 @@
---
title: Enforce Feature Boundaries Through Public APIs
impact: CRITICAL
impactDescription: Prevents architectural erosion and maintains loose coupling
tags: architecture, boundaries, imports, coupling
---
## Enforce Feature Boundaries Through Public APIs
**Impact: CRITICAL**
Features communicate through well-defined interfaces. If bookings needs availability data, it imports from `@calcom/features/availability` through exported interfaces, not by reaching into internal implementation details.
**Incorrect (reaching into internals):**
```typescript
// Bad - Importing internal implementation details
import { calculateSlots } from "@calcom/features/availability/services/internal/slotCalculator";
import { AvailabilityCache } from "@calcom/features/availability/lib/cache";
```
**Correct (using public API):**
```typescript
// Good - Import through the feature's public API
import { getAvailability } from "@calcom/features/availability";
import type { AvailabilityResult } from "@calcom/features/availability";
```
**Shared code placement:**
- Domain-agnostic utilities and cross-cutting concerns (auth, logging): `packages/lib`
- Shared UI primitives: `packages/ui`
**Enforcement:**
Domain boundaries are enforced automatically through linting. If `packages/features/bookings` tries to import from `packages/features/availability/services/internal`, the linter will block it. All cross-feature dependencies must go through the feature's public API.
**Benefits:**
- Discoverability: Looking for booking logic? It's all in `packages/features/bookings`
- Easier testing: Test the entire feature as a unit with all pieces in one place
- Clearer dependencies: When you see `import { getAvailability } from '@calcom/features/availability'`, you know exactly which feature you're depending on
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
@@ -0,0 +1,54 @@
---
title: packages/features vs apps/web/modules
impact: HIGH
impactDescription: Wrong placement causes tight coupling and import issues
tags: architecture, features, modules, trpc
---
# packages/features vs apps/web/modules
## packages/features
The `packages/features` package should contain only framework-agnostic code:
- Repositories (data access layer)
- Services (business logic)
- Core utilities and helpers
- Types and interfaces
**Files in `packages/features/**` should NOT import from `@calcom/trpc`.**
## apps/web/modules
Web-specific code, particularly anything that uses tRPC, should live in `apps/web/modules/...`:
- React hooks that use tRPC queries/mutations
- tRPC-specific utilities
- Web-only UI components that depend on tRPC
## Example Structure
```
packages/features/feature-opt-in/
├── repository/
│ └── FeatureOptInRepository.ts # Data access - OK here
├── service/
│ └── FeatureOptInService.ts # Business logic - OK here
└── types.ts # Types - OK here
apps/web/modules/feature-opt-in/
└── hooks/
└── useFeatureOptIn.ts # tRPC hook - MUST be here
```
## Why This Matters
```typescript
// ❌ Bad - tRPC hook in packages/features
// packages/features/feature-opt-in/hooks/useFeatureOptIn.ts
import { trpc } from "@calcom/trpc/react";
// ✅ Good - tRPC hook in apps/web/modules
// apps/web/modules/feature-opt-in/hooks/useFeatureOptIn.ts
import { trpc } from "@calcom/trpc/react";
```
This separation ensures that `packages/features` remains portable and can be used by other apps (like `apps/api/v2`) without pulling in web-specific dependencies.
@@ -0,0 +1,56 @@
---
title: Page-Level Authorization Checks in Next.js
impact: CRITICAL
impactDescription: Prevents unauthorized access to sensitive data
tags: security, nextjs, authorization, architecture
---
## Page-Level Authorization Checks in Next.js
**Impact: CRITICAL (Prevents unauthorized access to sensitive data)**
Authorization checks must be performed in `page.tsx` or server components, never in `layout.tsx`. Layouts don't intercept all requests and can be bypassed.
**Incorrect (auth checks in layout):**
```tsx
// app/admin/layout.tsx - DON'T DO THIS
export default async function AdminLayout({ children }) {
const session = await getUserSession();
if (!session?.user.role === "admin") {
redirect("/");
}
return <div>{children}</div>;
}
```
**Correct (auth checks in page):**
```tsx
// app/admin/page.tsx
import { redirect } from "next/navigation";
import { getUserSession } from "@/lib/auth";
export default async function AdminPage() {
const session = await getUserSession();
if (!session || session.user.role !== "admin") {
redirect("/"); // Or show an error
}
// Protected content here
return <div>Welcome, Admin!</div>;
}
```
**Why layouts are unsafe for auth:**
- Layouts don't intercept all requests (direct navigation, refreshes)
- APIs and server actions bypass layouts entirely
- Risk of data leaks if layout check is skipped
**Key rules:**
- Check permissions inside every restricted `page.tsx`
- Validate session/user/role before querying sensitive data
- Redirect or return nothing to unauthorized users before running restricted code
Reference: [Next.js Security Best Practices](https://nextjs.org/docs/app/building-your-application/authentication)
@@ -0,0 +1,61 @@
---
title: Organize Code by Domain Using Vertical Slices
impact: CRITICAL
impactDescription: Dramatically improves discoverability and reduces cross-team conflicts
tags: architecture, vertical-slices, ddd, organization
---
## Organize Code by Domain Using Vertical Slices
**Impact: CRITICAL**
Our codebase is organized by domain, not by technical layer. The `packages/features` directory is the heart of this architectural approach. Each folder inside represents a complete vertical slice of the application, driven by the domain it touches.
**Incorrect (traditional layered architecture):**
```
src/
controllers/
bookingController.ts
availabilityController.ts
services/
bookingService.ts
availabilityService.ts
repositories/
bookingRepository.ts
availabilityRepository.ts
```
This creates problems: changes to one feature require touching files scattered across multiple directories, it's hard to understand what a feature does because its code is fragmented, and teams step on each other's toes.
**Correct (vertical slice architecture):**
```
packages/features/
bookings/
services/
repositories/
components/
tests/
availability/
services/
repositories/
components/
tests/
```
Each feature folder is a self-contained vertical slice that includes:
- Domain logic: Core business rules and entities specific to that feature
- Application services: Use case orchestration for that domain
- Repositories: Data access specific to that feature's needs
- DTOs: Data transfer objects for crossing boundaries
- UI components: Frontend components related to this feature
- Tests: Unit, integration, and e2e tests for this feature
**Benefits:**
- Everything related to a feature lives in one directory
- You can understand the entire feature by exploring one directory
- Teams can work on different features without conflicts
- Features are loosely coupled and can evolve independently
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
+33
View File
@@ -0,0 +1,33 @@
---
title: CI Check Failure Handling
impact: HIGH
impactDescription: Misinterpreting CI failures wastes debugging time
tags: ci, debugging, workflow
---
# CI Check Failure Handling
## What to Focus On
When reviewing CI check failures in Cal.diy:
1. **E2E tests can be flaky** and may fail intermittently
2. **Focus only on CI failures that are directly related to your code changes**
3. Infrastructure-related failures (like dependency installation issues) can be disregarded if all code-specific checks pass
## Known CI Issues to Ignore
These errors are related to SAML database misconfiguration on CI and should be ignored:
- "password authentication failed for user postgres"
- "Invalid URL"
## E2E Tests Skipping
**E2E tests skipping is expected behavior:**
- When E2E tests are skipped, it's because the `ready-for-e2e` label has not been added to the PR
- The "required" check intentionally fails when E2E tests are skipped to prevent merging without E2E
- Do not try to fix anything related to skipped E2E tests - this is completely expected and normal
## Before Blaming CI
Always run type checks locally using `yarn type-check:ci --force` before concluding that CI failures are unrelated to your changes. Even if errors appear in files you haven't directly modified, your changes might still be causing type issues through dependencies or type inference.
+37
View File
@@ -0,0 +1,37 @@
---
title: Git and CI Workflow
impact: HIGH
impactDescription: Incorrect workflow causes wasted CI cycles and confusion
tags: git, ci, workflow
---
# Git and CI Workflow
## Push Before Checking CI
Always push committed changes to the remote repository before waiting for or checking CI status.
Waiting for CI checks on unpushed local commits is backwards - the CI runs on the remote repository state, not local commits.
**Proper sequence:**
1. Commit locally
2. Run local checks (`yarn type-check:ci --force`, `yarn biome check --write .`)
3. Push to remote
4. Monitor CI status
## Branch Operations
When asked to move changes to a different branch, use git commands to commit existing changes to the specified branch rather than redoing the work. This is more efficient and prevents duplication of effort.
## Never Force Push
**Never force push to main or production branches** - under any circumstances.
## Working with tRPC Changes
When making changes that affect tRPC components or after pulling updates that modify tRPC-related files:
1. First run `yarn prisma generate` to ensure all database types are up-to-date
2. Then run `cd packages/trpc && yarn build` to rebuild the tRPC package
This sequence ensures that type definitions are properly generated before building.
+43
View File
@@ -0,0 +1,43 @@
---
title: Type Check Before Tests
impact: HIGH
impactDescription: Type errors are often the root cause of test failures
tags: ci, typescript, type-check, workflow
---
# Type Check Before Tests
## Priority Order
When working on the Cal.diy repository, prioritize fixing type issues before addressing failing tests.
1. Run `yarn type-check:ci --force` first
2. Fix all TypeScript errors
3. Then run tests with `TZ=UTC yarn test`
## Why Type Check First
Type errors are often the root cause of test failures. Fixing types first:
- Eliminates cascading failures
- Ensures code compiles correctly
- Catches issues that tests might miss
## Comparing Branches
Compare type check results between the main branch and your feature branch to confirm whether you've introduced new type errors:
```bash
# On your branch
yarn type-check:ci --force 2>&1 | tee /tmp/feature-types.log
# On main
git checkout main
yarn type-check:ci --force 2>&1 | tee /tmp/main-types.log
# Compare
diff /tmp/main-types.log /tmp/feature-types.log
```
## Missing Enum Errors
If you encounter errors related to missing enum values (like `CreationSource.WEBAPP`), running `yarn prisma generate` will typically resolve these issues by regenerating the TypeScript types from the schema.
+45
View File
@@ -0,0 +1,45 @@
---
title: Hold Each Other Accountable for Quality
impact: MEDIUM
impactDescription: Builds collective ownership and prevents technical debt
tags: culture, accountability, quality, teamwork
---
## Hold Each Other Accountable for Quality
**Impact: MEDIUM**
We hold each other accountable for quality. Cutting corners might feel faster in the moment, but it creates problems that slow everyone down later. When you see a teammate about to merge a PR with obvious issues, speak up.
**This isn't about being difficult or slowing people down.** It's about collective ownership of our codebase and our reputation. Every shortcut one person takes becomes everyone's problem. Every corner cut today means more debugging sessions, more hotfixes, and more frustrated customers tomorrow.
**Make it normal to challenge poor decisions, respectfully:**
```
// When someone says:
"Let's just hard-code this for now"
// The expected response:
"What would it take to do it the proper way the first time?"
```
```
// When someone wants to commit untested code:
"Can we add tests for this before merging? I can help if needed."
```
```
// When someone suggests copy-paste instead of abstraction:
"This looks like it could be a shared utility. Should we extract it?"
```
**We're building something that needs to almost never fail.** That level of reliability doesn't happen by accident. It happens when every engineer feels responsible for quality - not just their own code but the entire system. We succeed as a team or we fail as a team.
**Key behaviors:**
- Push back when you see shortcuts being taken
- Offer to help when suggesting improvements
- Accept feedback gracefully when others challenge your decisions
- Focus on the code, not the person
- Remember that quality standards protect everyone
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
+75
View File
@@ -0,0 +1,75 @@
---
title: Leverage AI for Boilerplate and Testing
impact: MEDIUM
impactDescription: Accelerates development while maintaining quality
tags: culture, ai, automation, testing
---
## Leverage AI for Boilerplate and Testing
**Impact: MEDIUM**
Generate 80% of boilerplate and non-critical code using AI, allowing us to focus solely on complex business logic and critical architectures.
**Where AI excels:**
- Generating boilerplate code (DTOs, basic CRUD operations)
- Building comprehensive test suites
- Creating documentation
- Repetitive refactoring tasks
- Code review assistance
**Where humans must focus:**
- Complex business logic
- Critical architectural decisions
- Security-sensitive code
- Performance-critical algorithms
- Domain-specific edge cases
**Example - AI-assisted test generation:**
```typescript
// Human writes the function
export function calculateOverlap(slot: TimeSlot, busy: BusyTime): boolean {
return slot.start < busy.end && slot.end > busy.start;
}
// AI generates comprehensive tests
describe("calculateOverlap", () => {
it("returns true when slot starts during busy period", () => {
// AI-generated test case
});
it("returns true when slot ends during busy period", () => {
// AI-generated test case
});
it("returns false when slot is completely before busy period", () => {
// AI-generated test case
});
it("returns false when slot is completely after busy period", () => {
// AI-generated test case
});
it("returns true when slot completely contains busy period", () => {
// AI-generated test case
});
it("returns true when busy period completely contains slot", () => {
// AI-generated test case
});
// AI identifies edge cases humans might miss
it("handles exact boundary matches correctly", () => {
// AI-generated edge case
});
});
```
**Our CI is the final boss:**
- Everything in our standards document is checked before code is merged in PRs
- No surprises make it into main
- Checks are fast and useful
- AI helps ensure comprehensive coverage
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
+96
View File
@@ -0,0 +1,96 @@
---
title: Use DTOs at Every Architectural Boundary
impact: CRITICAL
impactDescription: Prevents technology coupling and security risks
tags: data, dto, boundaries, security, types
---
## Use DTOs at Every Architectural Boundary
**Impact: CRITICAL**
Database types should not leak to the frontend. This has become a popular shortcut in our tech stack, but it's a code smell that creates multiple problems.
**Problems with leaking database types:**
- Technology coupling (Prisma types end up in React components)
- Security risks (accidental leakage of sensitive fields)
- Fragile contracts between server and client
- Inability to evolve the database schema independently
**Incorrect (database types leaking):**
```typescript
// API route returning Prisma types directly
import type { User } from "@prisma/client";
export async function GET(): Promise<User> {
const user = await prisma.user.findFirst();
return user; // Leaks all database fields including sensitive ones
}
// Frontend using Prisma types
import type { User } from "@prisma/client";
function UserProfile({ user }: { user: User }) {
// Component now coupled to database schema
}
```
**Correct (explicit DTOs):**
```typescript
// Define explicit DTOs
interface UserDTO {
id: number;
name: string;
email: string;
// Only fields needed by the client
}
// API route transforms to DTO
export async function GET(): Promise<UserDTO> {
const user = await userRepository.findById(id);
return UserResponseSchema.parse(user); // Validate with Zod
}
// Frontend uses DTO
function UserProfile({ user }: { user: UserDTO }) {
// Component decoupled from database
}
```
**The standard:**
1. **Data layer → Application layer → API**: Transform database models into application-layer DTOs, then transform application DTOs into API-specific DTOs
2. **API → Application layer → Data layer**: Transform API DTOs through application layer and into data-specific DTOs
3. All DTO conversions through Zod to ensure all data is validated before sending to user
### DTO Location and Naming
**Location**: All DTOs go in `packages/lib/dto/`
**Naming conventions**:
- Base entity: `{Entity}Dto` (e.g., `BookingDto`)
- With relations: `{Entity}With{Relations}Dto` (e.g., `BookingWithAttendeesDto`)
- For specific projections: `{Entity}For{Purpose}Dto` (e.g., `BookingForConfirmationDto`)
- Avoid: `{Entity}Dto2`, `{Entity}DtoForHandler`, or other use-case-specific names
**Enum/union pattern** - use string literal unions to stay ORM-agnostic:
```typescript
// Good - ORM-agnostic string literal union
export type BookingStatusDto = "CANCELLED" | "ACCEPTED" | "REJECTED" | "PENDING";
// Bad - importing Prisma enum
import { BookingStatus } from "@calcom/prisma/client";
```
**Type safety** - never use `as any` in DTO mapping functions. If types don't align, fix the mapping explicitly.
### Prisma Boundaries
- **Allowed**: `packages/prisma`, repository implementations (`packages/features/**/repositories/*Repository.ts`), and low-level data access infrastructure.
- **Not allowed**: `packages/features/**` business logic (non-repository), `packages/trpc/**` handlers, `apps/web/**`, `apps/api/v2/**` services/controllers, and workflow/webhook/service layers.
Yes, this requires more code. Yes, it's worth it. Explicit boundaries prevent the architectural erosion that creates long-term maintenance nightmares.
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
@@ -0,0 +1,49 @@
---
title: Prefer Select Over Include in Prisma Queries
impact: HIGH
impactDescription: Reduces data transfer and improves query performance
tags: prisma, database, performance, security
---
## Prefer Select Over Include in Prisma Queries
**Impact: HIGH (Reduces data transfer and improves query performance)**
Using `select` instead of `include` in Prisma queries fetches only the fields you need, improving performance and preventing accidental exposure of sensitive data.
**Incorrect (using include fetches all fields):**
```typescript
const booking = await prisma.booking.findFirst({
include: {
user: true, // This gets ALL user fields including sensitive ones
}
});
```
**Correct (using select for specific fields):**
```typescript
const booking = await prisma.booking.findFirst({
select: {
id: true,
title: true,
user: {
select: {
id: true,
name: true,
email: true,
}
}
}
});
```
**Benefits:**
- **Performance**: Smaller payloads, faster queries
- **Security**: Prevents accidental exposure of sensitive fields (e.g., `credential.key`)
- **Clarity**: Makes data requirements explicit
**Exception:** Use `include` only when you genuinely need all fields from a relation, which is rare.
Reference: [Cal.diy Engineering Standards](https://cal.com/blog/engineering-in-2026-and-beyond)
@@ -0,0 +1,40 @@
---
title: Feature Flag Seeding
impact: MEDIUM
impactDescription: Proper feature flag setup enables controlled rollouts
tags: prisma, feature-flags, migrations
---
# Feature Flag Seeding
## Creating Feature Flag Migrations
To seed new feature flags in Cal.diy, create a Prisma migration:
```bash
yarn prisma migrate dev --create-only --name seed_[feature_name]_feature
```
## Migration File Location
The migration file should be placed in `packages/prisma/migrations/` with a timestamp prefix format:
```
20250724210733_seed_calendar_cache_sql_features/migration.sql
```
## SQL Structure
Follow the pattern from existing feature seeding migrations like:
`packages/prisma/migrations/20241216000000_add_calendar_cache_serve/migration.sql`
```sql
INSERT INTO "Feature" ("slug", "enabled", "type", "description", "createdAt", "updatedAt")
VALUES
('your-feature-slug', false, 'OPERATIONAL', 'Description of feature', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT ("slug") DO NOTHING;
```
The migration should INSERT the new features into the `Feature` table with:
- Appropriate type (like `OPERATIONAL`)
- Default `enabled` status for manual team enablement
+52
View File
@@ -0,0 +1,52 @@
---
title: Prisma Schema and Migrations
impact: HIGH
impactDescription: Schema changes affect all downstream code and deployments
tags: prisma, database, migrations, schema
---
# Prisma Schema and Migrations
## After Schema Changes
After making changes to the Prisma schema in Cal.diy and creating migrations, you need to run:
```bash
yarn prisma generate
```
This updates the TypeScript types. This is especially important:
- When switching Node.js versions
- After adding new fields to models
- After pulling changes that include Prisma schema updates
## Creating Migrations
```bash
# Development migration
npx prisma migrate dev --name migration_name
# Production deployment
yarn workspace @calcom/prisma db-deploy
```
## Timestamp Fields
When adding timestamp fields like `createdAt` and `updatedAt`:
- Do not set default values if you want existing records to have null values
- Only new records should get timestamps automatically
- For `updatedAt` fields, ensure they're updated when records are modified
## Squash Migrations
Whenever you change the schema.prisma file, remember to always consolidate migrations by squashing them as declared in the [Prisma docs](https://www.prisma.io/docs/orm/prisma-migrate/workflows/squashing-migrations).
This helps maintain a clean migration history and prevents accumulation of multiple migration files.
## Enum Generator Errors
If you encounter enum generator errors during the Prisma generate step (like "Cannot find module './enum-generator.ts'"), run `yarn install` first before trying to generate.
## Cache-Related Features
When implementing cache-related features that require timestamp tracking, always update the database schema first before modifying application code that references those fields.
+200
View File
@@ -0,0 +1,200 @@
---
title: Repository Method Naming Conventions
impact: HIGH
impactDescription: Improves code discoverability and reusability
tags: data, repository, naming, conventions, methods
---
## Repository Method Naming Conventions
**Impact: HIGH**
Repository methods should follow consistent naming conventions to improve discoverability and promote code reuse across different features.
### Rule 1: Don't include the repository's entity name in method names
Method names should be concise and avoid redundancy since the repository class name already indicates the entity type.
```typescript
// Good - Concise method names
class BookingRepository {
findById(id: string) { ... }
findByUserId(userId: string) { ... }
create(data: BookingCreateInput) { ... }
delete(id: string) { ... }
}
// Bad - Redundant entity name in methods
class BookingRepository {
findBookingById(id: string) { ... }
findBookingByUserId(userId: string) { ... }
createBooking(data: BookingCreateInput) { ... }
deleteBooking(id: string) { ... }
}
```
### Rule 2: Use `include` or similar keywords for methods that fetch relational data
When a method retrieves additional related entities, make this explicit in the method name using keywords like `include`, `with`, or `andRelations`.
```typescript
// Good - Clear indication of included relations
class EventTypeRepository {
findById(id: string) {
return prisma.eventType.findUnique({
where: { id },
});
}
findByIdIncludeHosts(id: string) {
return prisma.eventType.findUnique({
where: { id },
include: { hosts: true },
});
}
findByIdIncludeHostsAndSchedule(id: string) {
return prisma.eventType.findUnique({
where: { id },
include: { hosts: true, schedule: true },
});
}
}
// Bad - Unclear what data is included
class EventTypeRepository {
findById(id: string) {
return prisma.eventType.findUnique({
where: { id },
include: { hosts: true, schedule: true },
});
}
findByIdForReporting(id: string) {
return prisma.eventType.findUnique({
where: { id },
include: { hosts: true },
});
}
}
```
### Rule 3: Keep methods generic and reusable - avoid use-case-specific names
Repository methods should be general-purpose and describe what data they return, not how or where it's used. This promotes code reuse across different features.
```typescript
// Good - Generic, reusable methods
class BookingRepository {
findByUserIdIncludeAttendees(userId: string) {
return prisma.booking.findMany({
where: { userId },
include: { attendees: true },
});
}
findByDateRangeIncludeEventType(startDate: Date, endDate: Date) {
return prisma.booking.findMany({
where: {
startTime: { gte: startDate },
endTime: { lte: endDate },
},
include: { eventType: true },
});
}
}
// Bad - Use-case-specific method names
class BookingRepository {
findBookingsForReporting(userId: string) {
return prisma.booking.findMany({
where: { userId },
include: { attendees: true },
});
}
findBookingsForDashboard(startDate: Date, endDate: Date) {
return prisma.booking.findMany({
where: {
startTime: { gte: startDate },
endTime: { lte: endDate },
},
include: { eventType: true },
});
}
}
```
### Rule 4: No business logic in repositories
Repositories should only handle data access. Business logic, validations, and complex transformations belong in the Service layer.
```typescript
// Good - Repository only handles data access
class BookingRepository {
findByIdIncludeAttendees(id: string) {
return prisma.booking.findUnique({
where: { id },
include: { attendees: true },
});
}
updateStatus(id: string, status: BookingStatus) {
return prisma.booking.update({
where: { id },
data: { status },
});
}
}
class BookingService {
async confirmBooking(bookingId: string) {
const booking = await this.bookingRepository.findByIdIncludeAttendees(bookingId);
if (!booking) {
throw new Error("Booking not found");
}
if (booking.status !== "PENDING") {
throw new Error("Only pending bookings can be confirmed");
}
await this.emailService.sendConfirmationToAttendees(booking.attendees);
return this.bookingRepository.updateStatus(bookingId, "CONFIRMED");
}
}
// Bad - Business logic in repository
class BookingRepository {
async confirmBooking(bookingId: string) {
const booking = await prisma.booking.findUnique({
where: { id: bookingId },
include: { attendees: true },
});
if (!booking) {
throw new Error("Booking not found");
}
if (booking.status !== "PENDING") {
throw new Error("Only pending bookings can be confirmed");
}
await sendEmailToAttendees(booking.attendees);
return prisma.booking.update({
where: { id: bookingId },
data: { status: "CONFIRMED" },
});
}
}
```
### Summary
- Method names should be concise: `findById` not `findBookingById`
- Use `include`/`with` keywords when fetching relations: `findByIdIncludeHosts`
- Keep methods generic and reusable: `findByUserIdIncludeAttendees` not `findBookingsForReporting`
- No business logic in repositories - that belongs in Services
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
+65
View File
@@ -0,0 +1,65 @@
---
title: Isolate Technology Choices Behind Repositories
impact: CRITICAL
impactDescription: Enables technology changes without codebase-wide refactors
tags: data, repository, prisma, orm, isolation
---
## Isolate Technology Choices Behind Repositories
**Impact: CRITICAL**
Technology choices must not seep through the application. The Prisma problem illustrates this perfectly: we currently have references to Prisma scattered across hundreds of files. This creates massive coupling and makes technology changes prohibitively expensive.
**Incorrect (Prisma leaking throughout codebase):**
```typescript
// In a service file
import { prisma } from "@calcom/prisma";
async function getBooking(id: number) {
// Direct Prisma usage in service
return prisma.booking.findFirst({
where: { id },
include: { user: true }
});
}
```
**Correct (Repository abstraction):**
```typescript
// In repository file
import { prisma } from "@calcom/prisma";
export class BookingRepository {
async findById(id: number): Promise<BookingDTO | null> {
const booking = await prisma.booking.findFirst({
where: { id },
select: { id: true, title: true, userId: true }
});
return booking ? this.toDTO(booking) : null;
}
}
// In service file - no Prisma knowledge
import { BookingRepository } from "./repositories/BookingRepository";
async function getBooking(id: number) {
return this.bookingRepository.findById(id);
}
```
**The standard:**
- All database access must go through Repository classes
- Repositories are the only code that knows about Prisma (or any other ORM)
- No business logic should be in repositories
- Repositories are injected via Dependency Injection containers
**Benefits:**
If we ever switch from Prisma to Drizzle or another ORM, the only changes required are:
- Repository implementations
- DI container wiring for new repositories
- Nothing else in the codebase should care or change
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
+31
View File
@@ -0,0 +1,31 @@
---
title: App Store Integration Patterns
impact: MEDIUM
impactDescription: App store integrations require specific patterns for generated files
tags: app-store, integrations, generated-files
---
# App Store Integration Patterns
## Generated Files
The Cal.diy repository uses generated files (`*.generated.ts`) for app-store integrations. These files are created by the app-store-cli tool.
**Do not manually modify** `*.generated.ts` files. If you need structural changes to how integrations are imported or used, update the CLI code that generates these files.
## Import Patterns
Recent changes have moved from dynamic imports to static map-based imports for better performance. When working with browser components in the app-store, static imports should be used rather than dynamic imports.
### Generated File Types
When modifying the app-store-cli `build.ts` file, ensure it correctly handles all types:
1. **Regular service files** (`calendar.services.generated.ts`, `crm.services.generated.ts`, etc.) - need default imports
2. **Browser component files** (`apps.browser-addon.generated.tsx`, etc.) - may require dynamic imports with Next.js
The `lazyImport` parameter in `getExportedObject()` determines whether to use dynamic imports (for browser components) or static imports (for server-side services).
## Calendar Cache Features
The calendar cache system follows specific patterns in `packages/features/calendar-cache-sql`. When implementing provider-specific calendar cache services (like for Outlook/Office365), the provider-specific code should be placed in the corresponding provider directory (e.g., `packages/app-store/office365calendar`).
@@ -0,0 +1,280 @@
---
title: Use Dependency Injection for Loose Coupling
impact: HIGH
impactDescription: Enables build-time safety, testability, and maintainability
tags: patterns, dependency-injection, di, ioctopus, moduleloader, testing, coupling
---
## Use Dependency Injection for Loose Coupling
**Impact: HIGH**
Dependency Injection enables loose coupling, facilitates testing, and isolates concerns. Dependencies should be injected via DI containers rather than instantiated directly within classes.
**Incorrect (tight coupling with direct instantiation):**
```typescript
class BookingService {
private repository = new BookingRepository();
private emailService = new EmailService();
private calendarService = new GoogleCalendarService();
async createBooking(data: CreateBookingDTO) {
const booking = await this.repository.create(data);
await this.emailService.sendConfirmation(booking);
await this.calendarService.createEvent(booking);
return booking;
}
}
```
**Correct (dependency injection):**
```typescript
class BookingService {
constructor(
private readonly repository: BookingRepository,
private readonly emailService: EmailService,
private readonly calendarService: CalendarService,
) {}
async createBooking(data: CreateBookingDTO) {
const booking = await this.repository.create(data);
await this.emailService.sendConfirmation(booking);
await this.calendarService.createEvent(booking);
return booking;
}
}
```
**Required patterns:**
- **Application Services**: Orchestrate use cases, coordinate between domain services and repositories
- **Domain Services**: Contain business logic that doesn't naturally belong to a single entity
- **Repositories**: Abstract data access, isolate technology choices
- **Caching Proxies**: Wrap repositories or services to add caching behavior transparently
- **Decorators**: Add cross-cutting concerns (logging, metrics) without polluting domain logic
## Cal.diy's Type-Safe DI with moduleLoader
We use `@evyweb/ioctopus` to manage service and repository dependencies. The **moduleLoader pattern** provides type-safe dependency injection, ensuring that if a service adds a new dependency, TypeScript will catch missing dependencies at build time rather than runtime.
### Core Concepts
**Tokens**: Unique symbols that identify each service or repository in the DI container. Every injectable class needs a corresponding token. Tokens should be defined in a `tokens.ts` file within the feature's `di/` directory.
**Modules** (`.module.ts` files): Define how classes are instantiated and what dependencies they require using the `bindModuleToClassOnToken` function. Each module exports a `moduleLoader` object that knows how to load itself and its dependencies.
**Containers** (`.container.ts` files): Create a container instance and expose getter functions that consumers use to obtain service instances. Containers use the moduleLoader to automatically load all required dependencies.
**`bindModuleToClassOnToken`**: A type-safe function that binds a class to a token and declares its dependencies. TypeScript ensures the declared dependencies match what the class constructor expects.
### How It Works
**Step 1: Create tokens in the feature's di directory**
```typescript
// packages/features/myfeature/di/tokens.ts
export const MY_FEATURE_DI_TOKENS = {
MY_SERVICE: Symbol("MyService"),
MY_SERVICE_MODULE: Symbol("MyServiceModule"),
};
```
Then import these tokens in the central tokens file:
```typescript
// packages/features/di/tokens.ts
import { MY_FEATURE_DI_TOKENS } from "@calcom/features/myfeature/di/tokens";
export const DI_TOKENS = {
// ...existing tokens
...MY_FEATURE_DI_TOKENS,
};
```
**Step 2: Define the service class with constructor injection**
For services with multiple dependencies, use a dependencies interface:
```typescript
// packages/features/myfeature/services/MyService.ts
export interface IMyServiceDeps {
bookingRepo: BookingRepository;
userRepo: UserRepository;
}
export class MyService {
constructor(private deps: IMyServiceDeps) {}
async doSomething() {
const bookings = await this.deps.bookingRepo.findMany({...});
const user = await this.deps.userRepo.findById({...});
}
}
```
For services/repositories with a single dependency, pass it directly:
```typescript
// packages/features/myfeature/repositories/MyRepository.ts
export class MyRepository {
constructor(private prismaClient: PrismaClient) {}
async findById(id: string) {
return this.prismaClient.myModel.findUnique({ where: { id } });
}
}
```
**Step 3: Create a module file with moduleLoader**
```typescript
// packages/features/myfeature/di/MyService.module.ts
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { MyService } from "@calcom/features/myfeature/services/MyService";
import { moduleLoader as bookingRepositoryModuleLoader } from "./BookingRepository.module";
import { moduleLoader as userRepositoryModuleLoader } from "./UserRepository.module";
import { MY_FEATURE_DI_TOKENS } from "./tokens";
const thisModule = createModule();
const token = MY_FEATURE_DI_TOKENS.MY_SERVICE;
const moduleToken = MY_FEATURE_DI_TOKENS.MY_SERVICE_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: MyService,
depsMap: {
bookingRepo: bookingRepositoryModuleLoader,
userRepo: userRepositoryModuleLoader,
},
});
export const moduleLoader: ModuleLoader = {
token,
loadModule,
};
export type { MyService };
```
For a single dependency, use `dep` instead of `depsMap`:
```typescript
// packages/features/myfeature/di/MyRepository.module.ts
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: MyRepository,
dep: prismaModuleLoader,
});
```
**Step 4: Create a container that uses the moduleLoader**
```typescript
// packages/features/myfeature/di/MyService.container.ts
import { createContainer } from "@calcom/features/di/di";
import { type MyService, moduleLoader as myServiceModuleLoader } from "./MyService.module";
const myServiceContainer = createContainer();
export function getMyService(): MyService {
myServiceModuleLoader.loadModule(myServiceContainer);
return myServiceContainer.get<MyService>(myServiceModuleLoader.token);
}
```
**Step 5: Use the service via the container's getter function**
```typescript
import { getMyService } from "@calcom/features/myfeature/di/MyService.container";
const myService = getMyService();
await myService.doSomething();
```
## Common Mistakes to Avoid
**Mistake 1: Creating a repository or service class with all static methods**
```typescript
// Bad - Static methods bypass DI
export class BookingRepository {
static async findById(id: string) {
return prisma.booking.findUnique({ where: { id } });
}
}
// Good - Instance methods with constructor injection
export class BookingRepository {
constructor(private prismaClient: PrismaClient) {}
async findById(id: string) {
return this.prismaClient.booking.findUnique({ where: { id } });
}
}
```
**Mistake 2: Manually instantiating a class instead of using the DI container**
```typescript
// Bad - Manual instantiation bypasses DI
const bookingRepo = new BookingRepository(prisma);
const myService = new MyService({ bookingRepo });
// Good - Use the DI container's getter function
import { getMyService } from "@calcom/features/myfeature/di/MyService.container";
const myService = getMyService();
```
**Mistake 3: Importing Prisma directly in a service instead of using repository injection**
```typescript
// Bad - Service imports Prisma directly
import prisma from "@calcom/prisma";
export class MyService {
async doSomething() {
const bookings = await prisma.booking.findMany({...});
}
}
// Good - Service depends on repository via DI
export class MyService {
constructor(private deps: IMyServiceDeps) {}
async doSomething() {
const bookings = await this.deps.bookingRepo.findMany({...});
}
}
```
**Mistake 4: Manual module loading in containers (not type-safe)**
```typescript
// Bad - Manual module loading is not type-safe
const container = createContainer();
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
// Good - Use moduleLoader for type-safe dependency loading
export function getMyService(): MyService {
myServiceModuleLoader.loadModule(container);
return container.get<MyService>(myServiceModuleLoader.token);
}
```
## Why Use DI?
- **Build-time safety**: TypeScript catches missing dependencies before runtime
- **Testability**: Dependencies can be easily mocked in tests
- **Consistency**: All instances are created the same way with proper dependencies
- **Maintainability**: Changing a dependency only requires updating the module binding
- **Automatic dependency resolution**: The moduleLoader automatically loads all dependencies recursively
- **Self-documenting**: Each module declares its own dependencies explicitly
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
+87
View File
@@ -0,0 +1,87 @@
---
title: Use Factory Pattern to Push Conditionals to Entry Points
impact: HIGH
impactDescription: Keeps services focused and prevents complexity accumulation
tags: patterns, factory, conditionals, single-responsibility
---
## Use Factory Pattern to Push Conditionals to Entry Points
**Impact: HIGH**
If statements belong at the entry point, not scattered throughout your services. This is one of the most important architectural principles for maintaining clean, focused code that doesn't spiral into unmaintainable complexity.
**The problem with scattered conditionals:**
A service is written for a clear, specific purpose. Then a new product requirement arrives, and someone adds an if statement. A few years later, that service is littered with conditional checks. The service becomes:
- Complicated and hard to read
- Difficult to understand and reason about
- More susceptible to bugs
- Violating single responsibility
- Nearly impossible to test thoroughly
**Incorrect (conditionals scattered in service):**
```typescript
class BillingService {
async processPayment(entityId: number, entityType: string) {
if (entityType === "organization") {
// Organization-specific logic
const org = await this.getOrganization(entityId);
if (org.billingPlan === "enterprise") {
// More nested conditionals...
}
} else if (entityType === "team") {
// Team-specific logic
} else if (entityType === "user") {
// User-specific logic
}
}
}
```
**Correct (Factory pattern with specialized services):**
```typescript
// Factory makes the decision at entry point
class BillingServiceFactory {
static async createService(entityId: number): Promise<BillingService> {
const entity = await determineEntityType(entityId);
switch (entity.type) {
case "organization":
return new OrganizationBillingService(entity);
case "team":
return new TeamBillingService(entity);
default:
return new UserBillingService(entity);
}
}
}
// Each service handles ONLY its specific logic - no conditionals
class OrganizationBillingService extends BillingService {
async processPayment() {
// Only organization logic here - clean and focused
}
}
class TeamBillingService extends BillingService {
async processPayment() {
// Only team logic here - clean and focused
}
}
```
**Benefits:**
- Services stay focused with one responsibility
- Changes are isolated to specific service implementations
- Testing is straightforward - test each service independently
- New requirements don't pollute existing code
**Guidelines:**
- Push conditionals up to controllers, factories, or routing logic
- Keep services pure and focused on a single responsibility
- Prefer polymorphism over conditionals
- Watch for if statement accumulation during code review
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
+240
View File
@@ -0,0 +1,240 @@
---
title: Trigger.dev Task Implementation
impact: HIGH
impactDescription: Consistent Trigger.dev patterns ensure reliable async task execution across web and API v2
tags: trigger.dev, async, tasker, concurrency, cron, scheduled-tasks
---
# Trigger.dev Task Implementation
Trigger.dev is the async task runner used in both `apps/web` and `apps/api/v2`. Tasks can be disabled via the `ENABLE_ASYNC_TASKER` env var (set to `"false"` to fall back to synchronous execution).
## Tasker Architecture
Every Trigger.dev feature follows the Tasker pattern with these layers:
```
packages/features/<domain>/lib/tasker/
types.ts # ITasker interface + payload types
<Domain>Tasker.ts # Tasker subclass (dispatches async or sync)
<Domain>TriggerTasker.ts # Async implementation (calls .trigger())
<Domain>SyncTasker.ts # Sync fallback (executes inline)
<Domain>TaskService.ts # Business logic consumed by both
trigger/
config.ts # Queue + retry + machine config
schema.ts # Zod payload schema
<task-name>.ts # schemaTask definition
```
Reference implementation: `packages/features/calendars/lib/tasker/` — see `CalendarsTasker.ts` for the Tasker subclass pattern and `trigger/config.ts` for queue/retry/machine configuration
## Creating a New Task
### 1. Define the Zod schema for payload validation
Always use `schemaTask` with a Zod schema for payload validation:
```typescript
// trigger/schema.ts
import { z } from "zod";
export const myTaskSchema = z.object({
userId: z.number(),
});
```
### 2. Configure queue, retry, and machine
See `packages/features/calendars/lib/tasker/trigger/config.ts` for a real example.
```typescript
// trigger/config.ts
import { type schemaTask, queue } from "@trigger.dev/sdk";
type MyTask = Pick<Parameters<typeof schemaTask>[0], "machine" | "retry" | "queue">;
export const myQueue = queue({
name: "my-domain",
concurrencyLimit: 10,
});
export const myTaskConfig: MyTask = {
machine: "small-2x",
queue: myQueue,
retry: {
maxAttempts: 3,
factor: 2,
minTimeoutInMs: 60000,
maxTimeoutInMs: 300000,
randomize: true,
outOfMemory: {
machine: "medium-1x",
},
},
};
```
### 3. Define the task using schemaTask
```typescript
// trigger/my-task.ts
import { schemaTask, type TaskWithSchema } from "@trigger.dev/sdk";
import type { z } from "zod";
import { myTaskConfig } from "./config";
import { myTaskSchema } from "./schema";
export const MY_TASK_JOB_ID = "domain.my-task";
export const myTask: TaskWithSchema<typeof MY_TASK_JOB_ID, typeof myTaskSchema> =
schemaTask({
id: MY_TASK_JOB_ID,
...myTaskConfig,
schema: myTaskSchema,
run: async (payload: z.infer<typeof myTaskSchema>) => {
const { getMyService } = await import("@calcom/features/<domain>/di/<container>");
const service = getMyService();
await service.execute(payload);
},
});
```
### 4. For scheduled (cron) tasks, use `schedules.task`
```typescript
import { schedules } from "@trigger.dev/sdk";
export const myScheduledTask = schedules.task({
id: "domain.my-scheduled-task",
...myTaskConfig,
cron: {
pattern: "0 0 1 * *",
timezone: "UTC",
},
run: async (payload) => {
// task logic
},
});
```
Reference: [Trigger.dev Scheduled Tasks](https://trigger.dev/docs/tasks/scheduled)
## Concurrency Configuration
Concurrency limits control how many task runs execute in parallel within a queue. Getting this right is critical for production stability.
**How to estimate concurrency:**
1. Analyze production logs for the number of requests per minute that will trigger the task (from both `apps/web` and `apps/api/v2`)
2. Measure the average execution time of the task
3. Factor in burst patterns (e.g., peak booking hours)
**Guidelines:**
- Time-sensitive tasks (emails, webhooks) need higher concurrency
- Background jobs that are not time-sensitive can use lower concurrency
- Start conservative and increase based on production metrics
See `packages/features/calendars/lib/tasker/trigger/config.ts` (`concurrencyLimit: 10`) and `packages/features/webhooks/lib/tasker/trigger/config.ts` (`concurrencyLimit: 20`) for real examples of how concurrency is tuned per domain.
Reference: [Trigger.dev Concurrency & Queues](https://trigger.dev/docs/queue-concurrency)
## Local Development
1. Set env vars:
```
TRIGGER_SECRET_KEY=<your-trigger-secret>
TRIGGER_API_URL=https://api.trigger.dev
ENABLE_ASYNC_TASKER="true"
```
2. Run the Trigger.dev CLI from the features package:
```bash
cd packages/features && npx trigger.dev@latest dev --analyze
```
3. Keep the CLI running while developing. Tasks appear in the Trigger.dev dashboard under **Tasks**.
## Before Merging
1. **Deploy to staging**: `cd packages/features && yarn deploy:trigger:staging`
2. **Test on Trigger.dev dashboard**: Switch to staging environment and use the **Test** tab
3. **Right-size machines**: Start with the smallest machine; increase only if you see `OutOfMemory` errors
4. **Set retry with OOM handling**: Always include `outOfMemory` in retry config with a larger machine than the default
5. **Set concurrency**: Analyze production request volume and task completion time to determine the appropriate `concurrencyLimit`
6. **Cherry-pick caveat**: Cherry-picking does not redeploy Trigger.dev tasks. Only the `draft-release` CI action does. If cherry-picking a change that impacts Trigger.dev tasks, manually promote the new version in the Trigger.dev deployment dashboard after the fix is deployed
## Common Mistakes
**Using `task` instead of `schemaTask`:**
```typescript
// Bad - no payload validation
import { task } from "@trigger.dev/sdk";
export const myTask = task({
id: "my-task",
run: async (payload: any) => { ... },
});
// Good - validated payload with Zod
import { schemaTask } from "@trigger.dev/sdk";
export const myTask = schemaTask({
id: "my-task",
schema: myTaskSchema,
run: async (payload) => { ... },
});
```
**Missing retry or queue config:**
```typescript
// Bad - no retry or queue, will use defaults
export const myTask = schemaTask({
id: "my-task",
schema: myTaskSchema,
run: async (payload) => { ... },
});
// Good - explicit config from shared config file
import { myTaskConfig } from "./config";
export const myTask = schemaTask({
id: "my-task",
...myTaskConfig,
schema: myTaskSchema,
run: async (payload) => { ... },
});
```
**Importing eagerly inside task files instead of using dynamic imports:**
```typescript
// Bad - eager import of heavy modules at file scope
import { MyService } from "@calcom/features/domain/service/MyService";
export const myTask = schemaTask({
id: "my-task",
schema: myTaskSchema,
run: async (payload) => {
const service = new MyService();
await service.execute(payload);
},
});
// Good - dynamic import inside run function
export const myTask = schemaTask({
id: "my-task",
schema: myTaskSchema,
run: async (payload) => {
const { getMyService } = await import("@calcom/features/domain/di/container");
const service = getMyService();
await service.execute(payload);
},
});
```
## Key References
- Trigger.dev docs: [Concurrency & Queues](https://trigger.dev/docs/queue-concurrency), [Scheduled Tasks](https://trigger.dev/docs/tasks/scheduled), [schemaTask](https://trigger.dev/docs/tasks/schemaTask)
- Example taskers in the codebase:
- `packages/features/calendars/lib/tasker/` (standard pattern)
- `packages/features/webhooks/lib/tasker/` (webhook delivery)
- `packages/features/ee/billing/service/proration/tasker/` (scheduled cron task)
@@ -0,0 +1,54 @@
---
title: Avoid O(n²) Algorithms - Design for Enterprise Scale
impact: CRITICAL
impactDescription: Prevents performance collapse at scale
tags: performance, algorithms, complexity, scale
---
## Avoid O(n²) Algorithms - Design for Enterprise Scale
**Impact: CRITICAL**
We build for large organizations and teams. What works fine with 10 users or 50 records can collapse under the weight of enterprise scale. Performance is not something we optimize later. It's something we build correctly from the start.
When building features, always ask: "How does this behave with 1,000 users? 10,000 records? 100,000 operations?"
**Common O(n²) patterns to avoid:**
- Nested array iterations (`.map` inside `.map`, `.forEach` inside `.forEach`)
- Array methods like `.some`, `.find`, or `.filter` inside loops or callbacks
- Checking every item against every other item without optimization
- Chained filters or nested mapping over large lists
**Incorrect (O(n²) - exponential slowdown):**
```typescript
// Bad: O(n²) - checks every slot against every busy time
const available = availableSlots.filter(slot => {
return !busyTimes.some(busy => checkOverlap(slot, busy));
});
// For 100 slots and 50 busy periods: 5,000 checks
// For 500 slots and 200 busy periods: 100,000 checks (20x increase!)
```
**Correct (O(n log n) - scales gracefully):**
```typescript
// Good: O(n log n) - sort once, break early
const sortedBusy = [...busyTimes].sort((a, b) => a.start - b.start);
const available = availableSlots.filter(slot => {
// Binary search or early exit
const index = binarySearch(sortedBusy, slot.start);
return !hasOverlapAt(sortedBusy, index, slot);
});
```
**Better data structures and algorithms:**
- **Sorting + early exit**: Sort data once, break out of loops when remaining items won't match
- **Binary search**: Use for lookups in sorted arrays instead of linear scans
- **Two-pointer techniques**: For merging or intersecting sorted sequences
- **Hash maps/sets**: Use for O(1) lookups instead of `.find` or `.includes` on arrays
- **Interval trees**: For scheduling, availability, and range queries
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
+53
View File
@@ -0,0 +1,53 @@
---
title: Day.js Performance Guidelines
impact: HIGH
impactDescription: Significant performance improvement in date-heavy operations
tags: performance, dates, dayjs
---
## Day.js Performance Guidelines
**Impact: HIGH (Significant performance improvement in date-heavy operations)**
Day.js with the `@calcom/dayjs` wrapper is heavy because it pre-loads all plugins including locale handling. Use alternatives when strict timezone awareness isn't required.
**Incorrect (using Day.js unnecessarily):**
```typescript
// Slow in performance-critical code (loops)
dates.map((date) => dayjs(date).add(1, "day").format());
// Using Dayjs for simple date operations
const startOfMonth = dayjs().startOf("month");
```
**Correct (using performant alternatives):**
```typescript
// Use .utc() for better performance when timezone doesn't matter
dates.map((date) => dayjs.utc(date).add(1, "day").format());
// Use native Date when possible
dates.map((date) => new Date(date.valueOf() + 24 * 60 * 60 * 1000));
// Use date-fns for simple operations
import { startOfMonth, endOfDay } from "date-fns";
const monthStart = startOfMonth(dateObj);
const dayEnd = endOfDay(dateObj);
// For browser locale, use Intl with i18n
const { i18n: { language } } = useLocale();
new Intl.DateTimeFormat(language).format(date);
```
**When to use Day.js:**
- When you need strict timezone awareness (e.g., in the Booker)
- When working with complex timezone conversions
- When the performance impact is negligible (non-loop operations)
**When to avoid Day.js:**
- Simple date arithmetic
- Date formatting without timezone concerns
- Performance-critical loops over dates
Reference: [Cal.diy Engineering Standards](https://cal.com/blog/engineering-in-2026-and-beyond)
@@ -0,0 +1,58 @@
---
title: Handle NP-Hard Scheduling Problems Carefully
impact: HIGH
impactDescription: Prevents exponential blowup in scheduling operations
tags: performance, scheduling, algorithms, np-hard
---
## Handle NP-Hard Scheduling Problems Carefully
**Impact: HIGH**
Scheduling problems are fundamentally NP-hard. This means that as the number of constraints, participants, or time slots grows, the computational complexity can explode exponentially. Most optimal scheduling algorithms have worst-case exponential time complexity, making algorithm choice absolutely critical.
**Real-world implications:**
- Finding the optimal meeting time for 10 people across 3 time zones with individual availability constraints is computationally expensive
- Adding conflict detection, buffers, and other options amplifies the problem
- Poor algorithm choices that work fine for small teams become completely unusable for large organizations
- What takes milliseconds for 5 users might take many seconds for organizations
**Strategies for managing NP-hard complexity:**
```typescript
// Use approximation algorithms
async function findMeetingTime(participants: User[], duration: number) {
// Find "good enough" solution quickly rather than perfect solution slowly
const approximateSlots = await findApproximateAvailability(participants, {
maxIterations: 1000,
timeout: 500, // ms
});
return approximateSlots[0]; // Return first good-enough option
}
// Implement aggressive caching
const cachedAvailability = new LRUCache<string, Availability>({
max: 10000,
ttl: 1000 * 60 * 5, // 5 minutes
});
// Pre-compute common scenarios during off-peak hours
async function precomputeTeamAvailability(teamId: number) {
// Run during low-traffic periods
const team = await teamRepository.findById(teamId);
const availability = await computeTeamAvailability(team);
await cache.set(`team:${teamId}:availability`, availability);
}
```
**Key strategies:**
- Use approximation algorithms that find "good enough" solutions quickly
- Implement aggressive caching of computed schedules and availability
- Pre-compute common scenarios during off-peak hours
- Break large scheduling problems into smaller, more manageable chunks
- Set reasonable timeout limits and fallback to simpler algorithms when needed
This is why performance isn't just a nice-to-have in scheduling software. It's the foundation that determines whether your system can scale to enterprise needs.
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
@@ -0,0 +1,35 @@
---
title: Avoid Barrel Imports
impact: MEDIUM
impactDescription: Improves tree-shaking and reduces bundle size
tags: imports, performance, bundling
---
## Avoid Barrel Imports
**Impact: MEDIUM (Improves tree-shaking and reduces bundle size)**
Barrel files (index.ts that re-export from multiple modules) can hurt tree-shaking and increase bundle sizes. Import directly from source files instead.
**Incorrect (importing from barrel files):**
```typescript
// Importing from index.ts barrel files
import { BookingService, UserService } from "./services";
// Importing from @calcom/ui barrel
import { Button } from "@calcom/ui";
```
**Correct (importing directly from source):**
```typescript
// Import directly from source files
import { BookingService } from "./services/BookingService";
import { UserService } from "./services/UserService";
// Import directly from component path
import { Button } from "@calcom/ui/components/button";
```
Reference: [Cal.diy Engineering Standards](https://cal.com/blog/engineering-in-2026-and-beyond)
+51
View File
@@ -0,0 +1,51 @@
---
title: Code Comment Guidelines
impact: MEDIUM
impactDescription: Excessive comments add noise; missing comments hurt maintainability
tags: comments, documentation, readability
---
# Code Comment Guidelines
## General Principle
Keep comments limited and avoid obvious ones. Comments should explain "why" not "what" - the code itself should be clear enough to explain what it does.
## When to Comment
- Business decisions or domain logic that isn't obvious from the code
- Workarounds or hacks with explanation of why they're needed
- Non-obvious performance optimizations
- Important security considerations
- Troubleshooting context (e.g., why a particular approach was chosen after hitting issues)
If none of these apply, skip the comment entirely. The function name, parameters, and return type should speak for themselves.
## When NOT to Comment
```typescript
// ❌ Bad - Obvious comment
// Get the user
const user = await getUser(userId);
// ❌ Bad - Restating the code
// Loop through bookings
for (const booking of bookings) {
// Process booking
processBooking(booking);
}
```
## Good Examples
```typescript
// ✅ Good - Explains why, not what
// We need to fetch availability before slots because the timezone
// conversion depends on the user's configured availability rules
const availability = await getAvailability(userId);
const slots = convertToSlots(availability, timezone);
// ✅ Good - Documents a non-obvious constraint
// Google Calendar API has a 2500 event limit per sync request
const BATCH_SIZE = 2500;
```
+36
View File
@@ -0,0 +1,36 @@
---
title: Code Review Focus
impact: MEDIUM
impactDescription: Focused reviews are more useful than scattered feedback
tags: code-review, workflow
---
# Code Review Focus
## When Asked to Review a PR
Focus on providing a clear summary of what the PR is doing and its core functionality.
**Avoid getting sidetracked by:**
- CI failures
- Testing issues
- Technical implementation details (unless specifically requested)
## Good Review Structure
1. **Summary**: What does this PR do?
2. **Core changes**: What are the main code changes?
3. **Impact**: What parts of the system does this affect?
## What to Look For
- Does the code do what it claims to do?
- Are there any obvious bugs or edge cases?
- Does it follow Cal.diy coding standards?
- Is the change appropriately scoped?
## What to Skip (Unless Asked)
- Nitpicks about style (Biome handles this)
- Suggestions for refactoring unrelated code
- Deep dives into implementation details
+52
View File
@@ -0,0 +1,52 @@
---
title: Error Handling Patterns
impact: HIGH
impactDescription: Proper error handling ensures debuggable and secure code
tags: errors, trpc, services, repositories
---
# Error Handling Patterns
## Descriptive Errors
```typescript
// ✅ Good - Descriptive error with context
throw new Error(`Unable to create booking: User ${userId} has no available time slots for ${date}`);
// ❌ Bad - Generic error
throw new Error("Booking failed");
```
## ErrorWithCode vs TRPCError
Use `ErrorWithCode` for files that are not directly coupled to tRPC. The tRPC package has a middleware called `errorConversionMiddleware` that automatically converts `ErrorWithCode` instances into `TRPCError` instances.
### In Non-tRPC Files (services, repositories, utilities)
```typescript
import { ErrorCode } from "@calcom/lib/errorCodes";
import { ErrorWithCode } from "@calcom/lib/errors";
// Option 1: Using constructor with ErrorCode enum
throw new ErrorWithCode(ErrorCode.BookingNotFound, "Booking not found");
// Option 2: Using the Factory pattern for common HTTP errors
throw ErrorWithCode.Factory.Forbidden("You don't have permission to view this");
throw ErrorWithCode.Factory.NotFound("Resource not found");
throw ErrorWithCode.Factory.BadRequest("Invalid input");
```
### In tRPC Routers Only
```typescript
import { TRPCError } from "@trpc/server";
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid booking time slot",
});
```
## packages/features Import Restrictions
Files in `packages/features/**` should NOT import from `@calcom/trpc`. This keeps the features package decoupled from the tRPC layer, making the code more reusable and testable. Use `ErrorWithCode` for error handling in these files.
+49
View File
@@ -0,0 +1,49 @@
---
title: Import and Export Patterns
impact: MEDIUM
impactDescription: Incorrect imports cause build failures and bundle bloat
tags: imports, exports, modules, app-store
---
# Import and Export Patterns
## Named vs Default Exports
When working with imports in the Cal.diy codebase, particularly in app-store integrations, pay attention to whether modules use named exports or default exports.
Many services like VideoApiAdapter, CalendarService, and PaymentService are exported as named exports, but the actual export name may differ from the generic service type.
```typescript
// ✅ Good - Verify actual export name and use named import
import { AppleCalendarService } from "./applecalendar/lib/CalendarService";
// With renaming if needed
import { AppleCalendarService as ApplecalendarCalendarService } from "./applecalendar/lib/CalendarService";
// ❌ Bad - Assuming default export without checking
import CalendarService from "./applecalendar/lib/CalendarService";
```
## Generated Files
When fixing imports in Cal.diy's generated files (like `packages/app-store/apps.browser-*.generated.tsx`), always check the actual exports in the source files first.
For EventTypeAppCardInterface components, they likely use named exports rather than default exports, requiring:
```typescript
import * as ComponentName from "./path";
// instead of
import ComponentName from "./path";
```
## Factory Function Naming
When creating factory functions that replace class exports, use the naming convention `Build[ServiceName]` instead of just `[ServiceName]`:
```typescript
// ✅ Good - Clear factory function naming
export function BuildPaymentService() { ... }
// ❌ Bad - Confusing with class export
export function PaymentService() { ... }
```
+40
View File
@@ -0,0 +1,40 @@
---
title: Minimize Follow-up PRs for Small Refactors
impact: HIGH
impactDescription: Prevents technical debt accumulation
tags: quality, pr, refactoring, technical-debt
---
## Minimize Follow-up PRs for Small Refactors
**Impact: HIGH**
Follow-up PRs for minor improvements rarely materialize. Instead, they accumulate as technical debt that burdens us months or years later. If a small refactor can be done now, do it now.
**Incorrect:**
```typescript
// PR comment: "I'll fix this naming in a follow-up PR"
const d = getUserData(); // Bad variable name
const r = processData(d); // Another bad name
// The follow-up PR never happens, and now we have unclear code
```
**Correct:**
```typescript
// Fix it in the same PR
const userData = getUserData();
const processedResult = processData(userData);
```
**When follow-ups are acceptable:**
- Substantial changes that genuinely warrant separate PRs
- Exceptional, urgent cases where the current PR must ship immediately
- Changes that require additional review from different stakeholders
**The principle:**
The "slowness" of doing things right is an investment, not a cost. Code that's architected correctly from the start doesn't need massive refactors later.
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
+36
View File
@@ -0,0 +1,36 @@
---
title: PR Creation Best Practices
impact: HIGH
impactDescription: PRs that don't follow guidelines slow down review cycles
tags: pull-request, code-review, workflow
---
# PR Creation Best Practices
## Draft Mode
Create pull requests in draft mode by default, so that a human reviewer can mark it as ready for review only when it is.
## PR Title
- Use conventional commits: `feat:`, `fix:`, `refactor:`
- Be specific: `fix: handle timezone edge case in booking creation`
- Not generic: `fix: booking bug`
## Size Limits
- **Large PRs** (>500 lines or >10 files) are not recommended
- Split large changes by layer (database, backend, frontend)
- Split by feature component (API, UI, integration)
## PR Requirements
- PR title must follow Conventional Commits specification
- For most PRs, you only need to run linting and type checking
- E2E tests will only run if PR has "ready-for-e2e" label
## Before Pushing
1. Run `yarn type-check:ci --force` to check types
2. Run `yarn biome check --write .` to lint and format
3. Run relevant tests locally
+56
View File
@@ -0,0 +1,56 @@
---
title: Code Review Checklist
impact: HIGH
impactDescription: Ensures consistent code quality across all reviews
tags: quality, code-review, checklist
---
## Code Review Checklist
**Impact: HIGH**
A comprehensive checklist for code reviewers to ensure consistent quality standards.
### File Naming Conventions
- **Repository Files**: Must include `Repository` suffix, prefix with technology if applicable (e.g., `PrismaAppRepository.ts`), use PascalCase matching exported class
- **Service Files**: Must include `Service` suffix, use PascalCase matching exported class, avoid generic names (e.g., `MembershipService.ts`)
- **New files**: Avoid dot-suffixes like `.service.ts` or `.repository.ts`; reserve `.test.ts`, `.spec.ts`, `.types.ts` for their specific purposes
### Code Quality
- Prefer early returns. It is recommended to throw/return early so we can ensure null-checks and prevent further nesting.
- Prefer Composition over Prop Drilling. Instead of relying on prop drilling, let's try to take advantage of react children feature.
- For Prisma queries:
- Only select data you need
1) Increased performance overhead: When you select all columns from a table, Prisma retrieves all the data associated with each row, which can result in a significant performance impact. This includes reading unnecessary columns and transferring a large volume of data between your application and the database.
2) Unnecessary data exposure: Selecting all columns exposes sensitive data that might not be required by your application. This can pose security risks, especially if the table contains sensitive user information or proprietary data. (In our case app credentials and booking information)
- TLDR: Never use Includes - use select
- Select selects only the fields you specify explicitly
- Include selects the relationship AND all the fields of the table the relationship belongs to
- Make sure the credential.key field is never returned back from tRPC endpoints or APIs
- Always use t() for text localization in frontend code; direct text embedding should trigger a warning.
- Check if there's any O(n^2) logic in backend code; we should aim for O(n log n) or O(n) ideally.
- Check if there are circular references introduced. We should never allow these.
- Flag excessive Day.js use in performance-critical code. Functions like `.add`, `.diff`, `.isBefore`, and `.isAfter` are slow, especially in timezone mode. Prefer `.utc()` for better performance. Where possible, replace with native Date and direct `.valueOf()` comparisons for faster execution. Recommend using native methods or Day.js `.utc()` consistently in hot paths like loops.
### API Documentation
When docs changes are made in /docs/api-reference/v2/openapi.json, ensure the following:
- "summary" fields are short and concise
- "summary" fields do not end with a period
- "summary" fields are written in proper American English
### API Changes
When changes to API v2 or v1 are made, ensure there are no breaking changes on existing endpoints. Instead, there needs to be a newly versioned endpoint with the updated functionality but the old one must remain functional.
### PR Size
For large pull requests (>500 lines changed or >10 files touched), advise splitting into smaller, focused PRs:
- Split by feature boundaries: separate different features or user stories
- Split by layer/component: frontend changes, backend changes, database migrations, and tests in separate PRs
- Split by dependency chain: create PRs that can be merged sequentially
- Split by file/module: group related file changes together
- Suggested pattern: Database migrations → Backend logic → Frontend components → Integration tests
- Benefits: easier review, faster feedback, reduced conflicts, easier rollback, better history
+43
View File
@@ -0,0 +1,43 @@
---
title: Prioritize Clarity Over Cleverness
impact: HIGH
impactDescription: Reduces cognitive load and improves maintainability
tags: quality, simplicity, readability
---
## Prioritize Clarity Over Cleverness
**Impact: HIGH**
The goal is code that is easy to read and understand quickly, not elegant complexity. Simple systems reduce the cognitive load for every engineer.
**Questions to ask yourself:**
- Am I actually solving the problem at hand?
- Am I thinking too much about possible future use cases?
- Have I considered at least 1 other alternative for solving this? How does it compare?
**Incorrect (clever but hard to understand):**
```typescript
// Clever one-liner that's hard to parse
const result = data.reduce((a, b) => ({...a, [b.id]: (a[b.id] || []).concat(b)}), {});
```
**Correct (clear and readable):**
```typescript
// Clear, step-by-step approach
const groupedById: Record<string, Item[]> = {};
for (const item of data) {
if (!groupedById[item.id]) {
groupedById[item.id] = [];
}
groupedById[item.id].push(item);
}
```
**Important note:**
Simple doesn't mean lacking in features. Just because our goal is to create simple systems, this doesn't mean they should feel anemic and lacking obvious functionality.
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
@@ -0,0 +1,40 @@
---
title: Address All Nits Before Merging
impact: HIGH
impactDescription: Prevents codebase degradation over time
tags: quality, code-review, standards
---
## Address All Nits Before Merging
**Impact: HIGH**
Don't let PRs through with a lot of nits just to avoid being "the bad person." This is precisely how codebases become sloppy over time. Code review is not about being nice. It's about maintaining the quality standards our infrastructure demands.
**Incorrect approach:**
```
Reviewer: "This variable name could be clearer, but it's fine I guess"
Reviewer: "We usually use early returns here, but this works"
Reviewer: "Approved with minor suggestions"
// PR merged with multiple small issues
```
**Correct approach:**
```
Reviewer: "Please rename `d` to `userData` for clarity"
Reviewer: "Please refactor to use early returns per our standards"
Reviewer: "Requesting changes - please address before merging"
// PR updated to meet all standards before merge
```
**The principle:**
Every nitpick matters. Every pattern violation matters. Address them before merging, not after. We hold each other accountable for quality because cutting corners might feel faster in the moment, but it creates problems that slow everyone down later.
**Make it normal to challenge poor decisions, respectfully:**
- If someone says "let's just hard-code this for now," ask "what would it take to do it the proper way the first time?"
- If someone wants to commit untested code, push back
- If someone suggests copying and pasting instead of creating a proper abstraction, call it out respectfully
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
+50
View File
@@ -0,0 +1,50 @@
---
title: Key File Locations
impact: LOW
impactDescription: Quick reference for finding important files
tags: reference, navigation, file-locations
---
# Key File Locations
## UI Components
- Event types page: `apps/web/modules/event-types/views/event-types-listing-view.tsx`
- Bookings page: `apps/web/modules/bookings/views/bookings-view.tsx`
- Shared UI patterns (tabs, search bars, filter buttons) should maintain consistent alignment across views
## Database
- Schema: `packages/prisma/schema.prisma`
- Migrations: `packages/prisma/migrations/`
## API
- tRPC routers: `packages/trpc/server/routers/`
- API v2 controllers: `apps/api/v2/src/modules/*/controllers/*.controller.ts`
- OpenAPI spec: `docs/api-reference/v2/openapi.json` (auto-generated, don't edit manually)
## Features
- Workflow constants: `packages/features/ee/workflows/lib/constants.ts`
- Round-robin/host prioritization: `packages/features/bookings/lib/getLuckyUser.ts`
- Calendar cache: `packages/features/calendar-cache-sql`
- DataTable guide: `packages/features/data-table/GUIDE.md`
## Translations
- English: `packages/i18n/locales/en/common.json`
## App Store
- Generated files: `packages/app-store/*.generated.ts`
- CLI tool: `packages/app-store-cli/`
## File Naming Conventions
- **Repository files**: `PrismaBookingRepository.ts` (PascalCase with Repository suffix)
- **Service files**: `MembershipService.ts` (PascalCase with Service suffix)
- **Components**: `BookingForm.tsx` (PascalCase)
- **Utilities**: `date-utils.ts` (kebab-case)
- **Types**: `Booking.types.ts` (PascalCase with .types.ts suffix)
- **Tests**: Same as source file + `.test.ts` or `.spec.ts`
+73
View File
@@ -0,0 +1,73 @@
---
title: Local Development Setup
impact: LOW
impactDescription: Reference guide for local development environment
tags: reference, development, setup
---
# Local Development Setup
## Initial Setup
```bash
# Install dependencies
yarn
# Set up environment
cp .env.example .env
```
## Environment Variables
Generate required secrets:
```bash
# NEXTAUTH_SECRET
openssl rand -base64 32
# CALENDSO_ENCRYPTION_KEY (must be 32 characters for AES256)
openssl rand -base64 24
```
Configure in `.env`:
- `DATABASE_URL` - PostgreSQL connection string
- `DATABASE_DIRECT_URL` - Same as DATABASE_URL
## Database Setup
```bash
# Development
yarn workspace @calcom/prisma db-migrate
# Production
yarn workspace @calcom/prisma db-deploy
```
## Test Users
When setting up local development database, it creates test users. The passwords are the same as the username:
- `free:free`
- `pro:pro`
## Logging
Control logging verbosity by setting `NEXT_PUBLIC_LOGGER_LEVEL` in .env:
- 0: silly
- 1: trace
- 2: debug
- 3: info
- 4: warn
- 5: error
- 6: fatal
## API v2 Imports
If you need to import from `@calcom/features` or `@calcom/trpc` into `apps/api/v2`, use the platform-libraries package instead:
```typescript
// ✅ Good
import { SomeService } from "@calcom/platform-libraries";
// ❌ Bad - Will cause module resolution errors
import { SomeService } from "@calcom/features/...";
```
@@ -0,0 +1,67 @@
---
title: Maintain 80%+ Test Coverage for New Code
impact: HIGH
impactDescription: Prevents bugs and enables confident refactoring
tags: testing, coverage, quality, ci
---
## Maintain 80%+ Test Coverage for New Code
**Impact: HIGH**
Every PR must have near-80%+ test coverage for the code it introduces or modifies. This is enforced automatically in our CI pipeline. If you add 50 lines of new code, those 50 lines must be covered by tests. If you modify an existing function, your changes must be tested.
**Coverage requirements:**
- Overall test coverage: 80%+ for new code
- Unit test coverage: Near 100%, especially with AI assistance for generating tests
- Global coverage tracking as a key metric that improves over time
**Incorrect (untested code):**
```typescript
// New function with no tests
export function calculateAvailability(user: User, date: Date): TimeSlot[] {
// Complex logic here...
// No corresponding test file
}
```
**Correct (comprehensive tests):**
```typescript
// calculateAvailability.ts
export function calculateAvailability(user: User, date: Date): TimeSlot[] {
// Complex logic here...
}
// calculateAvailability.test.ts
describe("calculateAvailability", () => {
it("returns empty array for user with no schedule", () => {
const user = createMockUser({ schedules: [] });
expect(calculateAvailability(user, new Date())).toEqual([]);
});
it("excludes busy times from available slots", () => {
const user = createMockUser({
schedules: [mockSchedule],
busyTimes: [mockBusyTime],
});
const slots = calculateAvailability(user, new Date());
expect(slots).not.toContainEqual(expect.objectContaining({
start: mockBusyTime.start,
}));
});
it("handles timezone conversions correctly", () => {
// Test timezone edge cases
});
});
```
**Addressing the "coverage isn't the full story" argument:**
Yes, we know coverage doesn't guarantee perfect tests. We know you can write meaningless tests that hit every line but test nothing meaningful. We know coverage is just one metric among many. But it's surely better to shoot for a high percentage than to have no idea where you are at all.
**Leverage AI for test generation:**
AI can quickly and intelligently build comprehensive test suites. Manual testing is more and more a thing of the past.
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
+27
View File
@@ -0,0 +1,27 @@
---
title: Incremental Test Fixing
impact: MEDIUM
impactDescription: Methodical approach prevents getting overwhelmed by test failures
tags: testing, debugging, workflow
---
# Incremental Test Fixing
## One File at a Time
When fixing failing tests in the Cal.diy repository, take an incremental approach by addressing one file at a time rather than attempting to fix all issues simultaneously.
This methodical approach makes it easier to identify and resolve specific issues without getting overwhelmed by the complexity of multiple failing tests across different files.
## Recommended Order
1. Run `yarn type-check:ci --force` to identify TypeScript type errors
2. Run `yarn test` to identify failing unit tests
3. Address both type errors and failing tests before considering the task complete
4. Type errors often need to be fixed first as they may be causing the test failures
## Focus Strategy
- Focus on getting each file's tests passing completely before moving on to the next file
- Fix type errors before test failures - they're often the root cause
- Run `yarn prisma generate` if you see missing enum/type errors
+26
View File
@@ -0,0 +1,26 @@
---
title: Mock Implementation Patterns
impact: MEDIUM
impactDescription: Poor mocks cause flaky tests and false positives
tags: testing, mocking, calendar, app-store
---
# Mock Implementation Patterns
## Calendar Service Mocks
When mocking calendar services in Cal.diy test files, implement the `Calendar` interface rather than adding individual properties from each specific calendar service type (like `FeishuCalendarService`).
Since all calendar services implement the `Calendar` interface and are stored in a map, the mock service should also implement this interface to ensure type compatibility.
## App-Store Integration Mocks
When mocking app-store resources in Cal.diy tests, prefer implementing simpler mock designs that directly implement the required interfaces rather than trying to match complex deep mock structures created with `mockDeep`.
This approach is more maintainable and helps resolve type compatibility issues.
## General Guidance
- For complex mocks that cause type compatibility issues with deep mocks, consider using simpler fake implementations
- When needed, you can modify other mock files to support your implementation
- Creative solutions and refactoring to better designs are encouraged when standard mocking causes persistent type errors
+32
View File
@@ -0,0 +1,32 @@
---
title: Playwright E2E Testing
impact: HIGH
impactDescription: E2E tests catch integration issues before production
tags: testing, playwright, e2e
---
# Playwright E2E Testing
## Running Tests
Use the command format:
```bash
PLAYWRIGHT_HEADLESS=1 yarn e2e [test-file.e2e.ts]
```
This format includes the proper timezone setting, virtual display server, and uses the repository's e2e runner.
**Do not use** the standard `yarn playwright test` command.
## Local Testing First
Always ensure Playwright tests pass locally before pushing code. The user requires fast local e2e feedback loops instead of relying on CI, which is too slow for development iteration.
**Never push test code until those tests are passing locally first.**
## CI Behavior
- E2E tests will only run if PR has "ready-for-e2e" label
- When E2E tests are skipped, the "required" check intentionally fails to prevent merging without E2E
- Do not try to fix anything related to skipped E2E tests - this is expected behavior
+37
View File
@@ -0,0 +1,37 @@
---
title: Timezone Handling in Tests
impact: HIGH
impactDescription: Timezone bugs are hard to reproduce without consistent test environments
tags: testing, timezone, consistency
---
# Timezone Handling in Tests
## Always Use TZ=UTC
When running tests in the Cal.diy repository, use the `TZ=UTC` environment variable:
```bash
TZ=UTC yarn test
```
This ensures consistent timezone handling and prevents timezone-related test failures that might occur when tests are run in different environments or by different developers with varying local timezone settings.
## Why This Matters
- Tests may pass locally but fail in CI (or vice versa)
- Date/time assertions become unpredictable
- Debugging timezone issues is time-consuming
## Test Commands
```bash
# Unit tests
TZ=UTC yarn test
# Specific test file
TZ=UTC yarn vitest run path/to/file.test.ts
# E2E tests (already configured in yarn e2e)
PLAYWRIGHT_HEADLESS=1 yarn e2e
```
-1
View File
@@ -1 +0,0 @@
../agents/skills
+139
View File
@@ -0,0 +1,139 @@
---
name: calcom-api
description: Interact with the Cal.diy API v2 to manage scheduling, bookings, event types, availability, and calendars. Use this skill when building integrations that need to create or manage bookings, check availability, configure event types, or sync calendars with Cal.diy's scheduling infrastructure.
env:
CAL_API_KEY:
description: "Cal.diy API key (prefixed with cal_live_ or cal_test_). Required for all API requests."
required: true
CAL_CLIENT_ID:
description: "OAuth client ID for platform integrations managing users on behalf of others. Sent as x-cal-client-id header."
required: false
CAL_SECRET_KEY:
description: "OAuth client secret for platform integrations. Sent as x-cal-secret-key header."
required: false
CAL_WEBHOOK_SECRET:
description: "Secret used to verify webhook payload signatures via X-Cal-Signature-256 header."
required: false
---
# Cal.diy API v2
This skill provides guidance for AI agents to interact with the Cal.diy API v2, enabling scheduling automation, booking management, and calendar integrations.
## Base URL
All API requests should be made to:
```
https://api.cal.com/v2
```
## Required Credentials
| Environment Variable | Required | Description |
|---------------------|----------|-------------|
| `CAL_API_KEY` | Yes | Cal.diy API key (prefixed with `cal_live_` or `cal_test_`). Used as Bearer token for all API requests. Generate from Settings > Developer > API Keys. |
| `CAL_CLIENT_ID` | No | OAuth client ID for platform integrations that manage users on behalf of others. Sent as `x-cal-client-id` header. |
| `CAL_SECRET_KEY` | No | OAuth client secret for platform integrations. Sent as `x-cal-secret-key` header. |
| `CAL_WEBHOOK_SECRET` | No | Secret for verifying webhook payload signatures via the `X-Cal-Signature-256` header. |
## Authentication
All API requests require authentication via Bearer token:
```
Authorization: Bearer cal_<your_api_key>
```
For detailed authentication methods including OAuth/Platform authentication, see `references/authentication.md`.
## Core Concepts
**Event Types** define bookable meeting configurations (duration, location, availability rules). Each event type has a unique slug used in booking URLs.
**Bookings** are confirmed appointments created when someone books an event type. Each booking has a unique UID for identification.
**Schedules** define when a user is available for bookings. Users can have multiple schedules with different working hours.
**Slots** represent available time windows that can be booked based on event type configuration and user availability.
## Reference Documentation
This skill includes detailed API reference documentation for each domain:
| Reference | Description |
|-----------|-------------|
| `references/authentication.md` | API key and OAuth authentication, rate limiting, security best practices |
| `references/bookings.md` | Create, list, cancel, reschedule bookings |
| `references/event-types.md` | Configure bookable meeting types |
| `references/schedules.md` | Manage user availability schedules |
| `references/slots-availability.md` | Query available time slots |
| `references/calendars.md` | Calendar connections and busy times |
| `references/webhooks.md` | Real-time event notifications |
## Quick Start
### 1. Check Available Slots
Before creating a booking, check available time slots:
```http
GET /v2/slots?startTime=2024-01-15T00:00:00Z&endTime=2024-01-22T00:00:00Z&eventTypeId=123
```
See `references/slots-availability.md` for full details.
### 2. Create a Booking
```http
POST /v2/bookings
Content-Type: application/json
{
"start": "2024-01-15T10:00:00Z",
"eventTypeId": 123,
"attendee": {
"name": "John Doe",
"email": "john@example.com",
"timeZone": "America/New_York"
}
}
```
See `references/bookings.md` for all booking operations.
### 3. Set Up Webhooks
Receive real-time notifications for booking events:
```http
POST /v2/webhooks
Content-Type: application/json
{
"subscriberUrl": "https://your-app.com/webhook",
"triggers": ["BOOKING_CREATED", "BOOKING_CANCELLED"]
}
```
See `references/webhooks.md` for available triggers and payload formats.
## Common Workflows
**Book a meeting**: Check slots -> Create booking -> Store booking UID
**Reschedule**: Get new slots -> POST /v2/bookings/{uid}/reschedule
**Cancel**: POST /v2/bookings/{uid}/cancel with optional reason
## Best Practices
1. Always check slot availability before creating bookings
2. Store booking UIDs for future operations (cancel, reschedule)
3. Use ISO 8601 format for all timestamps
4. Implement webhook handlers for real-time updates
5. Handle rate limiting with exponential backoff
## Additional Resources
- [Full API Reference](https://cal.com/docs/api-reference/v2)
- [OpenAPI Specification](https://api.cal.com/v2/docs)
@@ -0,0 +1,406 @@
# Authentication API Reference
Detailed documentation for authentication methods in the Cal.diy API v2.
## Authentication Methods
Cal.diy API v2 supports two authentication methods:
1. **API Key Authentication** - For direct API access
2. **OAuth/Platform Authentication** - For platform integrations managing users on behalf of others
## API Key Authentication
The primary authentication method for most API consumers.
### Obtaining an API Key
1. Log in to your Cal.diy account
2. Navigate to Settings > Developer > API Keys
3. Click "Create new API key"
4. Copy and securely store the generated key
### Using API Keys
Include the API key in the `Authorization` header with the `Bearer` prefix:
```http
GET /v2/bookings
Authorization: Bearer cal_live_abc123xyz...
```
### API Key Format
All Cal.diy API keys are prefixed with `cal_`:
- `cal_live_...` - Production API keys
- `cal_test_...` - Test/sandbox API keys (if available)
### Example Request
```bash
curl -X GET "https://api.cal.com/v2/bookings" \
-H "Authorization: Bearer cal_live_abc123xyz789" \
-H "Content-Type: application/json"
```
## Refresh API Key
Generate a new API key and invalidate the current one:
```http
POST /v2/api-keys/refresh
Authorization: Bearer cal_live_current_key
Content-Type: application/json
{
"expiresAt": "2025-12-31T23:59:59Z"
}
```
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| expiresAt | string | No | ISO 8601 expiration date for the new key |
### Response
```json
{
"status": "success",
"data": {
"apiKey": "cal_live_new_key_xyz..."
}
}
```
## Platform Authentication (OAuth)
For platform customers building integrations that manage multiple users.
### Headers for Platform Authentication
Platform customers use additional headers alongside or instead of the Bearer token:
| Header | Description |
|--------|-------------|
| `x-cal-client-id` | OAuth client ID |
| `x-cal-secret-key` | OAuth client secret key |
| `Authorization` | Bearer token (managed user access token) |
### Example Platform Request
```bash
curl -X GET "https://api.cal.com/v2/bookings" \
-H "x-cal-client-id: your_client_id" \
-H "x-cal-secret-key: your_secret_key" \
-H "Authorization: Bearer managed_user_access_token" \
-H "Content-Type: application/json"
```
### When to Use Each Header
**For endpoints acting on behalf of a managed user:**
```http
GET /v2/bookings
x-cal-client-id: your_client_id
x-cal-secret-key: your_secret_key
Authorization: Bearer managed_user_access_token
```
**For platform-level operations (managing OAuth clients):**
```http
GET /v2/oauth-clients
Authorization: Bearer cal_live_platform_admin_key
```
## API Versioning
Many endpoints require a version header:
```http
cal-api-version: 2024-08-13
```
### Example with Version Header
```bash
curl -X POST "https://api.cal.com/v2/bookings" \
-H "Authorization: Bearer cal_live_abc123" \
-H "cal-api-version: 2024-08-13" \
-H "Content-Type: application/json" \
-d '{"start": "2024-01-15T10:00:00Z", "eventTypeId": 123, ...}'
```
## Authentication Errors
### 401 Unauthorized
Returned when authentication fails:
```json
{
"status": "error",
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key"
}
}
```
Common causes:
- Missing `Authorization` header
- Invalid or expired API key
- API key without `cal_` prefix
- Incorrect Bearer token format
### 403 Forbidden
Returned when authenticated but lacking permissions:
```json
{
"status": "error",
"error": {
"code": "FORBIDDEN",
"message": "You do not have permission to access this resource"
}
}
```
Common causes:
- Accessing another user's resources
- Missing required scopes for platform tokens
- Organization/team permission restrictions
## Security Best Practices
1. **Never expose API keys in client-side code**: API keys should only be used in server-side applications
2. **Use environment variables**: Store API keys in environment variables, not in code
```bash
export CAL_API_KEY="cal_live_abc123..."
```
3. **Rotate keys regularly**: Use the refresh endpoint to rotate keys periodically
4. **Use minimal permissions**: Request only the scopes/permissions your application needs
5. **Monitor API usage**: Check your Cal.diy dashboard for unusual activity
6. **Secure transmission**: Always use HTTPS for API requests
7. **Handle keys securely in logs**: Never log full API keys - redact sensitive portions
## Rate Limiting
API requests are rate limited. When exceeded, you'll receive:
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
```
```json
{
"status": "error",
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Please retry after 60 seconds."
}
}
```
### Rate Limit Headers
| Header | Description |
|--------|-------------|
| `X-RateLimit-Limit` | Maximum requests per window |
| `X-RateLimit-Remaining` | Remaining requests in current window |
| `X-RateLimit-Reset` | Unix timestamp when the window resets |
| `Retry-After` | Seconds to wait before retrying (on 429) |
## Testing Authentication
Verify your API key is working:
```bash
curl -X GET "https://api.cal.com/v2/me" \
-H "Authorization: Bearer cal_live_your_api_key" \
-H "Content-Type: application/json"
```
### Expected Response
```json
{
"status": "success",
"data": {
"id": 12345,
"email": "user@example.com",
"username": "johndoe",
"name": "John Doe",
"timeZone": "America/New_York"
}
}
```
## Common Authentication Patterns
### Server-Side Integration
```javascript
const CAL_API_KEY = process.env.CAL_API_KEY;
async function getBookings() {
const response = await fetch('https://api.cal.com/v2/bookings', {
headers: {
'Authorization': `Bearer ${CAL_API_KEY}`,
'Content-Type': 'application/json',
'cal-api-version': '2024-08-13'
}
});
if (!response.ok) {
if (response.status === 401) {
throw new Error('Invalid API key');
}
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
```
### Handling Token Refresh
```javascript
async function makeAuthenticatedRequest(url, options = {}) {
let response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${apiKey}`
}
});
if (response.status === 401) {
// Refresh the API key
const refreshResponse = await fetch('https://api.cal.com/v2/api-keys/refresh', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({})
});
if (refreshResponse.ok) {
const { data } = await refreshResponse.json();
apiKey = data.apiKey;
// Retry original request with new key
response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${apiKey}`
}
});
}
}
return response;
}
```
## Error Handling
The API returns standard HTTP status codes:
| Status Code | Description |
|-------------|-------------|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request (invalid parameters) |
| 401 | Unauthorized (invalid or missing API key) |
| 403 | Forbidden (insufficient permissions) |
| 404 | Not Found |
| 422 | Unprocessable Entity (validation error) |
| 429 | Too Many Requests (rate limited) |
| 500 | Internal Server Error |
### Error Response Format
```json
{
"status": "error",
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message"
}
}
```
### Common Error Codes
| Code | Description |
|------|-------------|
| UNAUTHORIZED | Invalid or missing authentication |
| FORBIDDEN | Insufficient permissions |
| NOT_FOUND | Resource not found |
| VALIDATION_ERROR | Invalid request parameters |
| RATE_LIMITED | Too many requests |
## Pagination
List endpoints support pagination via `take` and `skip` parameters:
| Parameter | Type | Default | Max | Description |
|-----------|------|---------|-----|-------------|
| take | number | 10 | 250 | Number of items to return |
| skip | number | 0 | - | Number of items to skip |
### Example
```http
GET /v2/bookings?take=20&skip=40
```
This returns items 41-60 (skipping the first 40, taking 20).
### Pagination Response
Some endpoints include pagination metadata:
```json
{
"status": "success",
"data": [...],
"pagination": {
"total": 150,
"take": 20,
"skip": 40
}
}
```
## Troubleshooting
### "Invalid API key" Error
1. Verify the key starts with `cal_`
2. Check for extra whitespace or characters
3. Ensure the key hasn't been revoked or expired
4. Confirm you're using the correct environment (production vs test)
### "Missing Authorization header" Error
1. Ensure the header name is exactly `Authorization`
2. Include the `Bearer ` prefix (with space)
3. Check for typos in the header name
### Platform Authentication Issues
1. Verify both `x-cal-client-id` and `x-cal-secret-key` are provided
2. Ensure the managed user access token is valid
3. Check that the OAuth client has the required permissions
@@ -0,0 +1,323 @@
# Bookings API Reference
Detailed documentation for booking-related endpoints in the Cal.diy API v2.
## Endpoints Overview
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /v2/bookings | List bookings |
| POST | /v2/bookings | Create a booking |
| GET | /v2/bookings/{bookingUid} | Get a booking |
| POST | /v2/bookings/{bookingUid}/cancel | Cancel a booking |
| POST | /v2/bookings/{bookingUid}/reschedule | Reschedule a booking |
| POST | /v2/bookings/{bookingUid}/confirm | Confirm a pending booking |
| POST | /v2/bookings/{bookingUid}/decline | Decline a booking |
| PATCH | /v2/bookings/{bookingUid}/location | Update booking location |
| POST | /v2/bookings/{bookingUid}/mark-absent | Mark attendee as no-show |
| POST | /v2/bookings/{bookingUid}/reassign | Reassign booking to another host |
| GET | /v2/bookings/{bookingUid}/references | Get booking references |
## List Bookings
```http
GET /v2/bookings
```
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| status | string | No | Filter by status: `upcoming`, `recurring`, `past`, `cancelled`, `unconfirmed` |
| attendeeEmail | string | No | Filter by attendee email |
| attendeeName | string | No | Filter by attendee name |
| eventTypeId | number | No | Filter by event type ID |
| eventTypeIds | string | No | Comma-separated event type IDs |
| teamsIds | string | No | Comma-separated team IDs |
| afterStart | string | No | Filter bookings starting after this ISO 8601 date |
| beforeEnd | string | No | Filter bookings ending before this ISO 8601 date |
| sortStart | string | No | Sort by start time: `asc` or `desc` |
| sortEnd | string | No | Sort by end time: `asc` or `desc` |
| sortCreated | string | No | Sort by creation time: `asc` or `desc` |
| take | number | No | Number of results (default: 10, max: 250) |
| skip | number | No | Pagination offset |
### Response
```json
{
"status": "success",
"data": [
{
"id": 12345,
"uid": "abc123def456",
"title": "30 Minute Meeting",
"description": "Discussion about project",
"start": "2024-01-15T10:00:00.000Z",
"end": "2024-01-15T10:30:00.000Z",
"status": "accepted",
"eventTypeId": 123,
"attendees": [
{
"name": "John Doe",
"email": "john@example.com",
"timeZone": "America/New_York"
}
],
"hosts": [
{
"id": 456,
"name": "Jane Smith",
"email": "jane@company.com"
}
],
"location": "https://cal.com/video/abc123",
"meetingUrl": "https://cal.com/video/abc123",
"metadata": {},
"createdAt": "2024-01-10T08:00:00.000Z"
}
]
}
```
## Create a Booking
```http
POST /v2/bookings
```
### Request Body
```json
{
"start": "2024-01-15T10:00:00Z",
"eventTypeId": 123,
"attendee": {
"name": "John Doe",
"email": "john@example.com",
"timeZone": "America/New_York",
"language": "en"
},
"guests": ["guest1@example.com", "guest2@example.com"],
"meetingUrl": "https://cal.com/team/meeting",
"metadata": {
"customField": "value"
},
"bookingFieldsResponses": {
"notes": "Please prepare the quarterly report"
}
}
```
### Required Fields
| Field | Type | Description |
|-------|------|-------------|
| start | string | ISO 8601 booking start time |
| eventTypeId | number | ID of the event type to book |
| attendee.name | string | Attendee's full name |
| attendee.email | string | Attendee's email address |
| attendee.timeZone | string | Attendee's timezone (IANA format) |
### Optional Fields
| Field | Type | Description |
|-------|------|-------------|
| attendee.language | string | Attendee's preferred language |
| guests | array | Additional guest email addresses |
| meetingUrl | string | Custom meeting URL |
| metadata | object | Custom metadata |
| bookingFieldsResponses | object | Responses to custom booking fields |
### Response
```json
{
"status": "success",
"data": {
"id": 12345,
"uid": "abc123def456",
"title": "30 Minute Meeting",
"start": "2024-01-15T10:00:00.000Z",
"end": "2024-01-15T10:30:00.000Z",
"status": "accepted",
"eventTypeId": 123,
"attendees": [...],
"hosts": [...],
"location": "https://cal.com/video/abc123"
}
}
```
## Get a Booking
```http
GET /v2/bookings/{bookingUid}
```
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| bookingUid | string | Unique booking identifier |
### Response
Returns the full booking object with all details.
## Cancel a Booking
```http
POST /v2/bookings/{bookingUid}/cancel
```
### Request Body
```json
{
"cancellationReason": "Schedule conflict"
}
```
### Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| cancellationReason | string | No | Reason for cancellation |
## Reschedule a Booking
```http
POST /v2/bookings/{bookingUid}/reschedule
```
### Request Body
```json
{
"start": "2024-01-16T14:00:00Z",
"reschedulingReason": "Conflict with another meeting"
}
```
### Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| start | string | Yes | New booking start time (ISO 8601) |
| reschedulingReason | string | No | Reason for rescheduling |
## Confirm a Booking
For event types that require confirmation:
```http
POST /v2/bookings/{bookingUid}/confirm
```
## Decline a Booking
```http
POST /v2/bookings/{bookingUid}/decline
```
### Request Body
```json
{
"reason": "Not available at this time"
}
```
## Update Booking Location
```http
PATCH /v2/bookings/{bookingUid}/location
```
### Request Body
```json
{
"location": "https://zoom.us/j/123456789"
}
```
## Mark Attendee as No-Show
```http
POST /v2/bookings/{bookingUid}/mark-absent
```
### Request Body
```json
{
"attendeeEmail": "john@example.com",
"noShow": true
}
```
## Reassign Booking
Reassign a booking to a different host:
```http
POST /v2/bookings/{bookingUid}/reassign
```
Or to a specific user:
```http
POST /v2/bookings/{bookingUid}/reassign/{userId}
```
## Get Booking References
Get external references (calendar events, video meetings) for a booking:
```http
GET /v2/bookings/{bookingUid}/references
```
### Response
```json
{
"status": "success",
"data": [
{
"type": "google_calendar",
"uid": "calendar-event-id",
"meetingUrl": "https://meet.google.com/abc-defg-hij"
}
]
}
```
## Booking Statuses
| Status | Description |
|--------|-------------|
| accepted | Booking is confirmed |
| pending | Awaiting confirmation |
| cancelled | Booking was cancelled |
| rejected | Booking was declined |
## Common Use Cases
### Book a Meeting
1. Get available slots: `GET /v2/slots?eventTypeId=123&startTime=...&endTime=...`
2. Create booking: `POST /v2/bookings` with selected slot
3. Store the booking UID for future operations
### Reschedule Flow
1. Get new available slots: `GET /v2/slots?eventTypeId=123&startTime=...&endTime=...`
2. Reschedule: `POST /v2/bookings/{uid}/reschedule` with new start time
### Cancel with Notification
1. Cancel: `POST /v2/bookings/{uid}/cancel` with reason
2. Attendees automatically receive cancellation emails
@@ -0,0 +1,327 @@
# Calendars API Reference
Detailed documentation for calendar integration endpoints in the Cal.diy API v2.
## Endpoints Overview
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /v2/calendars | List connected calendars |
| GET | /v2/calendars/busy-times | Get busy times |
| GET | /v2/calendars/{calendar}/check | Check calendar connection |
| POST | /v2/calendars/{calendar}/connect | Connect a calendar |
| DELETE | /v2/calendars/{calendar}/disconnect | Disconnect a calendar |
| GET | /v2/calendars/{calendar}/credentials | Get calendar credentials |
| GET | /v2/destination-calendars | List destination calendars |
| GET | /v2/selected-calendars | List selected calendars |
## List Connected Calendars
```http
GET /v2/calendars
```
### Response
```json
{
"status": "success",
"data": {
"connectedCalendars": [
{
"integration": {
"type": "google_calendar",
"title": "Google Calendar",
"slug": "google-calendar"
},
"credentialId": 123,
"primary": {
"externalId": "primary",
"name": "john@gmail.com",
"email": "john@gmail.com",
"isSelected": true,
"readOnly": false
},
"calendars": [
{
"externalId": "primary",
"name": "john@gmail.com",
"email": "john@gmail.com",
"isSelected": true,
"readOnly": false
},
{
"externalId": "calendar-id-2",
"name": "Work Calendar",
"email": "john@gmail.com",
"isSelected": true,
"readOnly": false
}
]
}
],
"destinationCalendar": {
"id": 1,
"integration": "google_calendar",
"externalId": "primary",
"name": "john@gmail.com"
}
}
}
```
## Get Busy Times
Check busy times from connected calendars to understand availability.
```http
GET /v2/calendars/busy-times
```
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| startTime | string | Yes | ISO 8601 start of date range |
| endTime | string | Yes | ISO 8601 end of date range |
| loggedInUsersTz | string | No | User's timezone |
| credentialId | number | No | Specific calendar credential ID |
### Example Request
```http
GET /v2/calendars/busy-times?startTime=2024-01-15T00:00:00Z&endTime=2024-01-22T00:00:00Z&loggedInUsersTz=America/New_York
```
### Response
```json
{
"status": "success",
"data": [
{
"start": "2024-01-15T10:00:00.000Z",
"end": "2024-01-15T11:00:00.000Z",
"title": "Team Meeting",
"source": "google_calendar"
},
{
"start": "2024-01-16T14:00:00.000Z",
"end": "2024-01-16T15:00:00.000Z",
"title": "Client Call",
"source": "google_calendar"
}
]
}
```
## Calendar Connection
### Check Calendar Connection
```http
GET /v2/calendars/{calendar}/check
```
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| calendar | string | Calendar type (e.g., `google-calendar`, `office365-calendar`) |
### Connect a Calendar
```http
POST /v2/calendars/{calendar}/connect
```
This initiates the OAuth flow for calendar connection.
### Disconnect a Calendar
```http
DELETE /v2/calendars/{calendar}/disconnect
```
## Supported Calendar Types
| Type | Slug | Description |
|------|------|-------------|
| Google Calendar | google-calendar | Google Workspace calendars |
| Microsoft 365 | office365-calendar | Outlook/Microsoft 365 calendars |
| Apple Calendar | apple-calendar | iCloud calendars (CalDAV) |
| CalDAV | caldav-calendar | Generic CalDAV calendars |
## Destination Calendars
The destination calendar is where new bookings are created.
### List Destination Calendars
```http
GET /v2/destination-calendars
```
### Response
```json
{
"status": "success",
"data": [
{
"id": 1,
"integration": "google_calendar",
"externalId": "primary",
"name": "john@gmail.com",
"userId": 123,
"eventTypeId": null,
"credentialId": 456
}
]
}
```
## Selected Calendars
Selected calendars are checked for conflicts when calculating availability.
### List Selected Calendars
```http
GET /v2/selected-calendars
```
### Response
```json
{
"status": "success",
"data": [
{
"integration": "google_calendar",
"externalId": "primary",
"credentialId": 123
},
{
"integration": "google_calendar",
"externalId": "work-calendar-id",
"credentialId": 123
}
]
}
```
## ICS Feed
### Check ICS Feed
```http
GET /v2/calendars/ics-feed/check
```
### Save ICS Feed
```http
POST /v2/calendars/ics-feed/save
```
```json
{
"url": "https://calendar.example.com/feed.ics"
}
```
## Calendar Events
### Get Calendar Event
```http
GET /v2/calendars/{calendar}/events/{eventUid}
```
### Response
```json
{
"status": "success",
"data": {
"id": "event-id-123",
"title": "Meeting",
"description": "Discussion",
"start": {
"time": "2024-01-15T10:00:00.000Z",
"timeZone": "America/New_York"
},
"end": {
"time": "2024-01-15T11:00:00.000Z",
"timeZone": "America/New_York"
},
"attendees": [
{
"email": "attendee@example.com",
"name": "Attendee Name",
"responseStatus": "accepted"
}
],
"status": "accepted",
"source": "google"
}
}
```
## Understanding Calendar Integration
### How Calendars Affect Availability
1. **Selected Calendars**: Events from selected calendars block availability
2. **Destination Calendar**: New bookings are created in this calendar
3. **Busy Times**: The API aggregates busy times from all selected calendars
### Calendar Sync Flow
```
1. User connects calendar (OAuth)
POST /v2/calendars/google-calendar/connect
2. User selects which calendars to check for conflicts
(Done via Cal.diy dashboard)
3. User sets destination calendar for new bookings
(Done via Cal.diy dashboard)
4. When checking slots:
- API fetches busy times from all selected calendars
- Busy times are excluded from available slots
5. When booking is created:
- Event is created in destination calendar
- Confirmation emails sent to attendees
```
### Cal.diy Event Identification
Cal.diy events in external calendars can be identified by their iCalUID ending with `@Cal.diy` (e.g., `2GBXSdEixretciJfKVmYN8@Cal.diy`).
## Team Calendar Integration
For team-level calendar management:
### Team Conferencing
```http
GET /v2/organizations/{orgId}/teams/{teamId}/conferencing
```
### Connect Team Calendar
```http
POST /v2/organizations/{orgId}/teams/{teamId}/conferencing/{app}/connect
```
## Best Practices
1. **Check connection status**: Verify calendar is connected before operations
2. **Handle OAuth expiry**: Calendar tokens may expire, handle re-authentication
3. **Respect rate limits**: Calendar APIs have their own rate limits
4. **Cache busy times**: Busy times don't change frequently, cache appropriately
5. **Use webhooks**: Subscribe to booking events instead of polling calendars
@@ -0,0 +1,439 @@
# Event Types API Reference
Detailed documentation for event type endpoints in the Cal.diy API v2.
## Endpoints Overview
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /v2/event-types | List event types |
| POST | /v2/event-types | Create an event type |
| GET | /v2/event-types/{eventTypeId} | Get an event type |
| PATCH | /v2/event-types/{eventTypeId} | Update an event type |
| DELETE | /v2/event-types/{eventTypeId} | Delete an event type |
## List Event Types
```http
GET /v2/event-types
```
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| take | number | No | Number of results (default: 10, max: 250) |
| skip | number | No | Pagination offset |
### Response
```json
{
"status": "success",
"data": [
{
"id": 123,
"title": "30 Minute Meeting",
"slug": "30min",
"description": "A quick 30 minute call",
"lengthInMinutes": 30,
"locations": [
{
"type": "integration",
"integration": "cal-video"
}
],
"bookingFields": [],
"disableGuests": false,
"slotInterval": null,
"minimumBookingNotice": 120,
"beforeEventBuffer": 0,
"afterEventBuffer": 0,
"schedulingType": null,
"metadata": {},
"requiresConfirmation": false,
"price": 0,
"currency": "usd",
"hidden": false
}
]
}
```
## Create an Event Type
```http
POST /v2/event-types
```
### Request Body
```json
{
"title": "30 Minute Meeting",
"slug": "30min",
"description": "A quick 30 minute call to discuss your needs",
"lengthInMinutes": 30,
"locations": [
{
"type": "integration",
"integration": "cal-video"
}
],
"bookingFields": [
{
"type": "textarea",
"name": "notes",
"label": "Additional Notes",
"required": false,
"placeholder": "Any additional information..."
}
],
"disableGuests": false,
"slotInterval": 15,
"minimumBookingNotice": 120,
"beforeEventBuffer": 5,
"afterEventBuffer": 5,
"scheduleId": 1,
"requiresConfirmation": false,
"hidden": false
}
```
### Required Fields
| Field | Type | Description |
|-------|------|-------------|
| title | string | Display name of the event type |
| slug | string | URL-friendly identifier |
| lengthInMinutes | number | Duration of the event |
### Optional Fields
| Field | Type | Description |
|-------|------|-------------|
| description | string | Description shown on booking page |
| locations | array | Meeting location options |
| bookingFields | array | Custom form fields |
| disableGuests | boolean | Prevent attendees from adding guests |
| slotInterval | number | Minutes between available slots |
| minimumBookingNotice | number | Minimum minutes before booking |
| beforeEventBuffer | number | Buffer time before event (minutes) |
| afterEventBuffer | number | Buffer time after event (minutes) |
| scheduleId | number | ID of schedule to use |
| requiresConfirmation | boolean | Require host confirmation |
| hidden | boolean | Hide from public profile |
## Location Types
### Cal Video (Built-in)
```json
{
"type": "integration",
"integration": "cal-video"
}
```
### Zoom
```json
{
"type": "integration",
"integration": "zoom"
}
```
### Google Meet
```json
{
"type": "integration",
"integration": "google-meet"
}
```
### Microsoft Teams
```json
{
"type": "integration",
"integration": "msteams"
}
```
### In-Person
```json
{
"type": "address",
"address": "123 Main St, City, Country"
}
```
### Phone Call (Host Calls)
```json
{
"type": "userPhone"
}
```
### Phone Call (Attendee Provides)
```json
{
"type": "attendeePhone"
}
```
### Custom Link
```json
{
"type": "link",
"link": "https://custom-meeting.com/room"
}
```
## Booking Fields
Custom form fields for collecting information from attendees.
### Text Field
```json
{
"type": "text",
"name": "company",
"label": "Company Name",
"required": true,
"placeholder": "Enter your company name"
}
```
### Textarea
```json
{
"type": "textarea",
"name": "notes",
"label": "Additional Notes",
"required": false
}
```
### Select (Dropdown)
```json
{
"type": "select",
"name": "topic",
"label": "Meeting Topic",
"required": true,
"options": [
{ "value": "sales", "label": "Sales Inquiry" },
{ "value": "support", "label": "Support" },
{ "value": "other", "label": "Other" }
]
}
```
### Radio Buttons
```json
{
"type": "radio",
"name": "preference",
"label": "Preferred Contact Method",
"required": true,
"options": [
{ "value": "email", "label": "Email" },
{ "value": "phone", "label": "Phone" }
]
}
```
### Checkbox
```json
{
"type": "checkbox",
"name": "terms",
"label": "I agree to the terms",
"required": true
}
```
### Phone Number
```json
{
"type": "phone",
"name": "phone",
"label": "Phone Number",
"required": false
}
```
## Get an Event Type
```http
GET /v2/event-types/{eventTypeId}
```
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| eventTypeId | number | Event type ID |
## Update an Event Type
```http
PATCH /v2/event-types/{eventTypeId}
```
### Request Body
Only include fields you want to update:
```json
{
"title": "Updated Meeting Title",
"lengthInMinutes": 45,
"hidden": false
}
```
## Delete an Event Type
```http
DELETE /v2/event-types/{eventTypeId}
```
## Team Event Types
For team event types, use the team-scoped endpoints:
### List Team Event Types
```http
GET /v2/teams/{teamId}/event-types
```
### Create Team Event Type
```http
POST /v2/teams/{teamId}/event-types
```
Additional fields for team event types:
```json
{
"title": "Team Meeting",
"slug": "team-meeting",
"lengthInMinutes": 30,
"schedulingType": "ROUND_ROBIN",
"hosts": [
{ "userId": 1, "isFixed": false },
{ "userId": 2, "isFixed": false }
]
}
```
### Scheduling Types
| Type | Description |
|------|-------------|
| ROUND_ROBIN | Distributes bookings among team members |
| COLLECTIVE | All team members must attend |
| MANAGED | Parent event type that creates child event types |
## Private Links
Create private booking links that bypass public visibility:
### List Private Links
```http
GET /v2/event-types/{eventTypeId}/private-links
```
### Create Private Link
```http
POST /v2/event-types/{eventTypeId}/private-links
```
## Organization Event Types
For organization-level event type management:
### List Organization Teams
```http
GET /v2/organizations/{orgId}/teams
```
### Response
```json
{
"status": "success",
"data": [
{
"id": 1,
"name": "Sales Team",
"slug": "sales",
"parentId": null,
"isOrganization": false
}
]
}
```
### Get Team Event Types (Organization Scope)
```http
GET /v2/organizations/{orgId}/teams/{teamId}/event-types
```
### Create Team Event Type (Organization Scope)
```http
POST /v2/organizations/{orgId}/teams/{teamId}/event-types
```
Team event types support different scheduling modes:
| Mode | Description |
|------|-------------|
| COLLECTIVE | All team members must attend the meeting |
| ROUND_ROBIN | Distributes bookings among team members based on availability and priority |
| MANAGED | Parent event type that creates child event types for team members |
## Event Type Webhooks
Configure webhooks specific to an event type:
### List Event Type Webhooks
```http
GET /v2/event-types/{eventTypeId}/webhooks
```
### Create Event Type Webhook
```http
POST /v2/event-types/{eventTypeId}/webhooks
```
```json
{
"subscriberUrl": "https://your-app.com/webhook",
"triggers": ["BOOKING_CREATED"],
"active": true
}
```
@@ -0,0 +1,359 @@
# Schedules API Reference
Detailed documentation for schedule management endpoints in the Cal.diy API v2.
## Endpoints Overview
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /v2/schedules | List all schedules |
| POST | /v2/schedules | Create a schedule |
| GET | /v2/schedules/default | Get default schedule |
| GET | /v2/schedules/{scheduleId} | Get a schedule |
| PATCH | /v2/schedules/{scheduleId} | Update a schedule |
| DELETE | /v2/schedules/{scheduleId} | Delete a schedule |
## Understanding Schedules
Schedules define when a user is available for bookings. Key concepts:
- **Working Hours**: Regular weekly availability (e.g., Mon-Fri 9am-5pm)
- **Date Overrides**: Exceptions to regular hours (e.g., holiday, special hours)
- **Timezone**: The timezone in which availability is defined
- **Default Schedule**: The primary schedule used when no specific schedule is assigned
## List Schedules
```http
GET /v2/schedules
```
### Response
```json
{
"status": "success",
"data": [
{
"id": 1,
"name": "Working Hours",
"isDefault": true,
"timeZone": "America/New_York",
"workingHours": [
{
"days": [1, 2, 3, 4, 5],
"startTime": 540,
"endTime": 1020
}
],
"availability": [
{
"id": 1,
"days": [1, 2, 3, 4, 5],
"startTime": "1970-01-01T09:00:00.000Z",
"endTime": "1970-01-01T17:00:00.000Z"
}
],
"dateOverrides": [],
"isManaged": false,
"readOnly": false,
"isLastSchedule": false
}
]
}
```
## Create a Schedule
```http
POST /v2/schedules
```
### Request Body
```json
{
"name": "Working Hours",
"timeZone": "America/New_York",
"isDefault": true,
"availability": [
{
"days": [1, 2, 3, 4, 5],
"startTime": "09:00",
"endTime": "17:00"
}
],
"dateOverrides": [
{
"date": "2024-12-25",
"startTime": null,
"endTime": null
}
]
}
```
### Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| name | string | Yes | Schedule name |
| timeZone | string | Yes | IANA timezone identifier |
| isDefault | boolean | No | Set as default schedule |
| availability | array | Yes | Weekly availability rules |
| dateOverrides | array | No | Date-specific overrides |
### Availability Object
| Field | Type | Description |
|-------|------|-------------|
| days | array | Days of week (0=Sunday, 1=Monday, ..., 6=Saturday) |
| startTime | string | Start time in HH:MM format |
| endTime | string | End time in HH:MM format |
### Date Override Object
| Field | Type | Description |
|-------|------|-------------|
| date | string | Date in YYYY-MM-DD format |
| startTime | string/null | Start time (null = unavailable) |
| endTime | string/null | End time (null = unavailable) |
## Get Default Schedule
```http
GET /v2/schedules/default
```
Returns the user's default schedule.
## Get a Schedule
```http
GET /v2/schedules/{scheduleId}
```
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| scheduleId | number | Schedule ID |
## Update a Schedule
```http
PATCH /v2/schedules/{scheduleId}
```
### Request Body
Only include fields you want to update:
```json
{
"name": "Updated Schedule Name",
"availability": [
{
"days": [1, 2, 3, 4, 5],
"startTime": "08:00",
"endTime": "18:00"
}
]
}
```
## Delete a Schedule
```http
DELETE /v2/schedules/{scheduleId}
```
Note: You cannot delete your last schedule. At least one schedule must exist.
## Common Schedule Patterns
### Standard Business Hours (Mon-Fri 9-5)
```json
{
"name": "Business Hours",
"timeZone": "America/New_York",
"availability": [
{
"days": [1, 2, 3, 4, 5],
"startTime": "09:00",
"endTime": "17:00"
}
]
}
```
### Split Schedule (Morning and Afternoon)
```json
{
"name": "Split Hours",
"timeZone": "America/New_York",
"availability": [
{
"days": [1, 2, 3, 4, 5],
"startTime": "09:00",
"endTime": "12:00"
},
{
"days": [1, 2, 3, 4, 5],
"startTime": "14:00",
"endTime": "18:00"
}
]
}
```
### Different Hours Per Day
```json
{
"name": "Variable Hours",
"timeZone": "America/New_York",
"availability": [
{
"days": [1, 3, 5],
"startTime": "09:00",
"endTime": "17:00"
},
{
"days": [2, 4],
"startTime": "10:00",
"endTime": "19:00"
}
]
}
```
### Weekend Availability
```json
{
"name": "Weekend Support",
"timeZone": "America/New_York",
"availability": [
{
"days": [0, 6],
"startTime": "10:00",
"endTime": "14:00"
}
]
}
```
### Holiday Override (Unavailable)
```json
{
"dateOverrides": [
{
"date": "2024-12-25",
"startTime": null,
"endTime": null
},
{
"date": "2024-01-01",
"startTime": null,
"endTime": null
}
]
}
```
### Special Hours Override
```json
{
"dateOverrides": [
{
"date": "2024-12-24",
"startTime": "09:00",
"endTime": "12:00"
}
]
}
```
## Organization/Team Schedules
For organization-level schedule management:
### List Organization Schedules
```http
GET /v2/organizations/{orgId}/schedules
```
### List User Schedules in Organization
```http
GET /v2/organizations/{orgId}/users/{userId}/schedules
```
### Create User Schedule in Organization
```http
POST /v2/organizations/{orgId}/users/{userId}/schedules
```
### Team Member Schedules
```http
GET /v2/organizations/{orgId}/teams/{teamId}/users/{userId}/schedules
```
## Working Hours Format
The API returns working hours in two formats:
### Minutes from Midnight
```json
{
"workingHours": [
{
"days": [1, 2, 3, 4, 5],
"startTime": 540,
"endTime": 1020
}
]
}
```
- `540` = 9:00 AM (9 * 60 minutes)
- `1020` = 5:00 PM (17 * 60 minutes)
### ISO Time Format
```json
{
"availability": [
{
"days": [1, 2, 3, 4, 5],
"startTime": "1970-01-01T09:00:00.000Z",
"endTime": "1970-01-01T17:00:00.000Z"
}
]
}
```
When creating/updating, use HH:MM format:
```json
{
"startTime": "09:00",
"endTime": "17:00"
}
```
## Best Practices
1. **Always specify timezone**: Schedules are timezone-aware
2. **Use date overrides sparingly**: For recurring patterns, create separate schedules
3. **Test availability**: After creating a schedule, verify with the slots endpoint
4. **Consider buffer times**: Set buffers on event types, not schedules
@@ -0,0 +1,255 @@
# Slots and Availability API Reference
Detailed documentation for checking availability and managing slots in the Cal.diy API v2.
## Endpoints Overview
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /v2/slots | Get available time slots |
| POST | /v2/slots/reservations | Reserve a slot temporarily |
| DELETE | /v2/slots/reservations/{uid} | Release a reserved slot |
| GET | /v2/calendars/busy-times | Get busy times from calendars |
## Get Available Slots
Check available time slots for booking an event type.
```http
GET /v2/slots
```
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| startTime | string | Yes | ISO 8601 start of date range |
| endTime | string | Yes | ISO 8601 end of date range |
| eventTypeId | number | Conditional | Event type ID (required if no slug) |
| eventTypeSlug | string | Conditional | Event type slug (required if no ID) |
| usernameList | string | Conditional | Comma-separated usernames for team events |
| timeZone | string | No | Timezone for slot display (default: UTC) |
| duration | number | No | Override event duration in minutes |
| rescheduleUid | string | No | Booking UID if rescheduling |
### Example Request
```http
GET /v2/slots?startTime=2024-01-15T00:00:00Z&endTime=2024-01-22T00:00:00Z&eventTypeId=123&timeZone=America/New_York
```
### Response
```json
{
"status": "success",
"data": {
"slots": {
"2024-01-15": [
{
"time": "2024-01-15T09:00:00.000Z"
},
{
"time": "2024-01-15T09:30:00.000Z"
},
{
"time": "2024-01-15T10:00:00.000Z"
}
],
"2024-01-16": [
{
"time": "2024-01-16T09:00:00.000Z"
}
]
}
}
}
```
### Response with Attendees (Seated Events)
For event types with `seatsPerTimeSlot` configured:
```json
{
"status": "success",
"data": {
"slots": {
"2024-01-15": [
{
"time": "2024-01-15T09:00:00.000Z",
"attendees": 3,
"seatsAvailable": 7
}
]
}
}
}
```
## Reserve a Slot
Temporarily reserve a slot while the user completes the booking form. This prevents double-booking.
```http
POST /v2/slots/reservations
```
### Request Body
```json
{
"eventTypeId": 123,
"slotUtcStartDate": "2024-01-15T09:00:00.000Z",
"slotUtcEndDate": "2024-01-15T09:30:00.000Z"
}
```
### Response
```json
{
"status": "success",
"data": {
"uid": "reservation-uid-123",
"eventTypeId": 123,
"slotUtcStartDate": "2024-01-15T09:00:00.000Z",
"slotUtcEndDate": "2024-01-15T09:30:00.000Z",
"expiresAt": "2024-01-15T08:10:00.000Z"
}
}
```
Reservations automatically expire after a short period (typically 10 minutes).
## Release a Reserved Slot
Release a slot reservation if the user abandons the booking flow.
```http
DELETE /v2/slots/reservations/{uid}
```
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| uid | string | Reservation UID |
## Get Busy Times
Check busy times from connected calendars.
```http
GET /v2/calendars/busy-times
```
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| startTime | string | Yes | ISO 8601 start of date range |
| endTime | string | Yes | ISO 8601 end of date range |
| loggedInUsersTz | string | No | User's timezone |
| credentialId | number | No | Specific calendar credential ID |
### Response
```json
{
"status": "success",
"data": [
{
"start": "2024-01-15T10:00:00.000Z",
"end": "2024-01-15T11:00:00.000Z",
"title": "Existing Meeting",
"source": "google_calendar"
}
]
}
```
## Routing Form Slots
For routing forms that direct to different event types:
```http
POST /v2/routing-forms/{routingFormId}/calculate-slots
```
### Request Body
```json
{
"startTime": "2024-01-15T00:00:00Z",
"endTime": "2024-01-22T00:00:00Z",
"timeZone": "America/New_York",
"responses": {
"field1": "value1",
"field2": "value2"
}
}
```
## Understanding Slot Availability
Slots are calculated based on:
1. **User's Schedule**: Working hours defined in their schedule
2. **Existing Bookings**: Times already booked
3. **Calendar Busy Times**: Events from connected calendars
4. **Buffer Times**: Before/after event buffers
5. **Minimum Notice**: Minimum booking notice period
6. **Booking Limits**: Daily/weekly/monthly booking limits
## Best Practices
### Efficient Slot Fetching
1. **Limit date range**: Request only the dates you need to display
2. **Cache results**: Slots don't change frequently, cache for short periods
3. **Use timezone parameter**: Request slots in user's timezone to avoid conversion
### Preventing Double Bookings
1. **Reserve slots**: Use slot reservations for multi-step booking flows
2. **Handle expiration**: Reservations expire - handle gracefully
3. **Verify before booking**: Always verify slot is still available before creating booking
### Example Booking Flow
```
1. User selects date range
GET /v2/slots?startTime=...&endTime=...&eventTypeId=123
2. User selects a slot
POST /v2/slots/reservations
{
"eventTypeId": 123,
"slotUtcStartDate": "2024-01-15T09:00:00Z",
"slotUtcEndDate": "2024-01-15T09:30:00Z"
}
3. User fills booking form
4. Create booking
POST /v2/bookings
{
"start": "2024-01-15T09:00:00Z",
"eventTypeId": 123,
"attendee": {...}
}
5. If user abandons, release reservation
DELETE /v2/slots/reservations/{uid}
```
## Timezone Handling
All times in the API are in UTC (ISO 8601 format). Use the `timeZone` parameter to receive slots in a specific timezone for display purposes.
```http
GET /v2/slots?startTime=2024-01-15T00:00:00Z&endTime=2024-01-22T00:00:00Z&eventTypeId=123&timeZone=Europe/London
```
The response times will still be in UTC, but the slot calculation will respect the user's timezone for determining available hours.
@@ -0,0 +1,330 @@
# Webhooks API Reference
Detailed documentation for webhook management endpoints in the Cal.diy API v2.
## Endpoints Overview
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /v2/webhooks | List webhooks |
| POST | /v2/webhooks | Create a webhook |
| GET | /v2/webhooks/{webhookId} | Get a webhook |
| PATCH | /v2/webhooks/{webhookId} | Update a webhook |
| DELETE | /v2/webhooks/{webhookId} | Delete a webhook |
## List Webhooks
```http
GET /v2/webhooks
```
### Response
```json
{
"status": "success",
"data": [
{
"id": "webhook-id-123",
"subscriberUrl": "https://your-app.com/webhook",
"triggers": ["BOOKING_CREATED", "BOOKING_CANCELLED"],
"active": true,
"payloadTemplate": null,
"secret": "whsec_..."
}
]
}
```
## Create a Webhook
```http
POST /v2/webhooks
```
### Request Body
```json
{
"subscriberUrl": "https://your-app.com/webhook",
"triggers": ["BOOKING_CREATED", "BOOKING_CANCELLED", "BOOKING_RESCHEDULED"],
"active": true,
"payloadTemplate": null,
"secret": "your-webhook-secret"
}
```
### Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| subscriberUrl | string | Yes | URL to receive webhook payloads |
| triggers | array | Yes | Events that trigger the webhook |
| active | boolean | No | Enable/disable webhook (default: true) |
| payloadTemplate | string | No | Custom payload template |
| secret | string | No | Secret for signature verification |
## Webhook Triggers
| Trigger | Description |
|---------|-------------|
| BOOKING_CREATED | New booking created |
| BOOKING_CANCELLED | Booking cancelled |
| BOOKING_RESCHEDULED | Booking rescheduled |
| BOOKING_CONFIRMED | Pending booking confirmed |
| BOOKING_REJECTED | Booking rejected |
| BOOKING_REQUESTED | Booking request received (requires confirmation) |
| BOOKING_PAYMENT_INITIATED | Payment started |
| BOOKING_NO_SHOW_UPDATED | Attendee marked as no-show |
| MEETING_STARTED | Video meeting started |
| MEETING_ENDED | Video meeting ended |
| RECORDING_READY | Meeting recording available |
| INSTANT_MEETING | Instant meeting created |
| RECORDING_TRANSCRIPTION_GENERATED | Transcription ready |
| FORM_SUBMITTED | Routing form submitted |
## Webhook Payload
### BOOKING_CREATED Payload
```json
{
"triggerEvent": "BOOKING_CREATED",
"createdAt": "2024-01-15T10:00:00.000Z",
"payload": {
"type": "30min",
"title": "30 Minute Meeting",
"description": "Meeting description",
"startTime": "2024-01-15T10:00:00.000Z",
"endTime": "2024-01-15T10:30:00.000Z",
"organizer": {
"id": 123,
"name": "Jane Smith",
"email": "jane@company.com",
"timeZone": "America/New_York"
},
"attendees": [
{
"name": "John Doe",
"email": "john@example.com",
"timeZone": "America/New_York"
}
],
"location": "https://cal.com/video/abc123",
"destinationCalendar": {
"integration": "google_calendar",
"externalId": "calendar-id"
},
"uid": "booking-uid-123",
"metadata": {},
"responses": {
"name": "John Doe",
"email": "john@example.com"
}
}
}
```
### BOOKING_CANCELLED Payload
```json
{
"triggerEvent": "BOOKING_CANCELLED",
"createdAt": "2024-01-15T12:00:00.000Z",
"payload": {
"uid": "booking-uid-123",
"title": "30 Minute Meeting",
"startTime": "2024-01-15T10:00:00.000Z",
"endTime": "2024-01-15T10:30:00.000Z",
"cancellationReason": "Schedule conflict",
"organizer": {...},
"attendees": [...]
}
}
```
### BOOKING_RESCHEDULED Payload
```json
{
"triggerEvent": "BOOKING_RESCHEDULED",
"createdAt": "2024-01-15T11:00:00.000Z",
"payload": {
"uid": "new-booking-uid",
"rescheduleUid": "original-booking-uid",
"title": "30 Minute Meeting",
"startTime": "2024-01-16T14:00:00.000Z",
"endTime": "2024-01-16T14:30:00.000Z",
"reschedulingReason": "Conflict with another meeting",
"organizer": {...},
"attendees": [...]
}
}
```
## Signature Verification
Webhooks include a signature header for verification:
```
X-Cal-Signature-256: sha256=<signature>
```
### Verification Example (Node.js)
```javascript
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return `sha256=${expectedSignature}` === signature;
}
// In your webhook handler
app.post('/webhook', (req, res) => {
const signature = req.headers['x-cal-signature-256'];
const payload = JSON.stringify(req.body);
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process webhook
const { triggerEvent, payload: data } = req.body;
// ...
res.status(200).send('OK');
});
```
## Get a Webhook
```http
GET /v2/webhooks/{webhookId}
```
## Update a Webhook
```http
PATCH /v2/webhooks/{webhookId}
```
### Request Body
```json
{
"active": false,
"triggers": ["BOOKING_CREATED"]
}
```
## Delete a Webhook
```http
DELETE /v2/webhooks/{webhookId}
```
## Event Type Webhooks
Create webhooks specific to an event type:
### List Event Type Webhooks
```http
GET /v2/event-types/{eventTypeId}/webhooks
```
### Create Event Type Webhook
```http
POST /v2/event-types/{eventTypeId}/webhooks
```
```json
{
"subscriberUrl": "https://your-app.com/webhook",
"triggers": ["BOOKING_CREATED"],
"active": true
}
```
## Organization Webhooks
For organization-wide webhooks:
### List Organization Webhooks
```http
GET /v2/organizations/{orgId}/webhooks
```
### Create Organization Webhook
```http
POST /v2/organizations/{orgId}/webhooks
```
## OAuth Client Webhooks
For platform integrations:
### List OAuth Client Webhooks
```http
GET /v2/oauth-clients/{clientId}/webhooks
```
### Create OAuth Client Webhook
```http
POST /v2/oauth-clients/{clientId}/webhooks
```
## Custom Payload Templates
Customize webhook payloads using templates:
```json
{
"subscriberUrl": "https://your-app.com/webhook",
"triggers": ["BOOKING_CREATED"],
"payloadTemplate": "{\"event\": \"{{triggerEvent}}\", \"booking_id\": \"{{payload.uid}}\", \"attendee\": \"{{payload.attendees[0].email}}\"}"
}
```
### Available Template Variables
- `{{triggerEvent}}` - Event type
- `{{payload.uid}}` - Booking UID
- `{{payload.title}}` - Event title
- `{{payload.startTime}}` - Start time
- `{{payload.endTime}}` - End time
- `{{payload.organizer.name}}` - Organizer name
- `{{payload.organizer.email}}` - Organizer email
- `{{payload.attendees[0].name}}` - First attendee name
- `{{payload.attendees[0].email}}` - First attendee email
## Best Practices
1. **Always verify signatures**: Use the webhook secret to verify payloads
2. **Respond quickly**: Return 200 within 5 seconds, process async if needed
3. **Handle retries**: Webhooks are retried on failure, implement idempotency
4. **Use HTTPS**: Always use HTTPS endpoints for security
5. **Log payloads**: Store webhook payloads for debugging
6. **Monitor failures**: Track webhook delivery failures
## Retry Policy
Failed webhook deliveries are retried with exponential backoff:
- 1st retry: 1 minute
- 2nd retry: 5 minutes
- 3rd retry: 30 minutes
- 4th retry: 2 hours
- 5th retry: 24 hours
After 5 failed attempts, the webhook is marked as failed.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,125 @@
---
name: vercel-react-best-practices
description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
license: MIT
metadata:
author: vercel
version: "1.0.0"
---
# Vercel React Best Practices
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 45 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
## When to Apply
Reference these guidelines when:
- Writing new React components or Next.js pages
- Implementing data fetching (client or server-side)
- Reviewing code for performance issues
- Refactoring existing React/Next.js code
- Optimizing bundle size or load times
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
| 3 | Server-Side Performance | HIGH | `server-` |
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
| 6 | Rendering Performance | MEDIUM | `rendering-` |
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
| 8 | Advanced Patterns | LOW | `advanced-` |
## Quick Reference
### 1. Eliminating Waterfalls (CRITICAL)
- `async-defer-await` - Move await into branches where actually used
- `async-parallel` - Use Promise.all() for independent operations
- `async-dependencies` - Use better-all for partial dependencies
- `async-api-routes` - Start promises early, await late in API routes
- `async-suspense-boundaries` - Use Suspense to stream content
### 2. Bundle Size Optimization (CRITICAL)
- `bundle-barrel-imports` - Import directly, avoid barrel files
- `bundle-dynamic-imports` - Use next/dynamic for heavy components
- `bundle-defer-third-party` - Load analytics/logging after hydration
- `bundle-conditional` - Load modules only when feature is activated
- `bundle-preload` - Preload on hover/focus for perceived speed
### 3. Server-Side Performance (HIGH)
- `server-cache-react` - Use React.cache() for per-request deduplication
- `server-cache-lru` - Use LRU cache for cross-request caching
- `server-serialization` - Minimize data passed to client components
- `server-parallel-fetching` - Restructure components to parallelize fetches
- `server-after-nonblocking` - Use after() for non-blocking operations
### 4. Client-Side Data Fetching (MEDIUM-HIGH)
- `client-swr-dedup` - Use SWR for automatic request deduplication
- `client-event-listeners` - Deduplicate global event listeners
### 5. Re-render Optimization (MEDIUM)
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
- `rerender-memo` - Extract expensive work into memoized components
- `rerender-dependencies` - Use primitive dependencies in effects
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
- `rerender-functional-setstate` - Use functional setState for stable callbacks
- `rerender-lazy-state-init` - Pass function to useState for expensive values
- `rerender-transitions` - Use startTransition for non-urgent updates
### 6. Rendering Performance (MEDIUM)
- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
- `rendering-content-visibility` - Use content-visibility for long lists
- `rendering-hoist-jsx` - Extract static JSX outside components
- `rendering-svg-precision` - Reduce SVG coordinate precision
- `rendering-hydration-no-flicker` - Use inline script for client-only data
- `rendering-activity` - Use Activity component for show/hide
- `rendering-conditional-render` - Use ternary, not && for conditionals
### 7. JavaScript Performance (LOW-MEDIUM)
- `js-batch-dom-css` - Group CSS changes via classes or cssText
- `js-index-maps` - Build Map for repeated lookups
- `js-cache-property-access` - Cache object properties in loops
- `js-cache-function-results` - Cache function results in module-level Map
- `js-cache-storage` - Cache localStorage/sessionStorage reads
- `js-combine-iterations` - Combine multiple filter/map into one loop
- `js-length-check-first` - Check array length before expensive comparison
- `js-early-exit` - Return early from functions
- `js-hoist-regexp` - Hoist RegExp creation outside loops
- `js-min-max-loop` - Use loop for min/max instead of sort
- `js-set-map-lookups` - Use Set/Map for O(1) lookups
- `js-tosorted-immutable` - Use toSorted() for immutability
### 8. Advanced Patterns (LOW)
- `advanced-event-handler-refs` - Store event handlers in refs
- `advanced-use-latest` - useLatest for stable callback refs
## How to Use
Read individual rule files for detailed explanations and code examples:
```
rules/async-parallel.md
rules/bundle-barrel-imports.md
rules/_sections.md
```
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
- Additional context and references
## Full Compiled Document
For the complete guide with all rules expanded: `AGENTS.md`
@@ -0,0 +1,55 @@
---
title: Store Event Handlers in Refs
impact: LOW
impactDescription: stable subscriptions
tags: advanced, hooks, refs, event-handlers, optimization
---
## Store Event Handlers in Refs
Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
**Incorrect (re-subscribes on every render):**
```tsx
function useWindowEvent(event: string, handler: (e: Event) => void) {
useEffect(() => {
window.addEventListener(event, handler)
return () => window.removeEventListener(event, handler)
}, [event, handler])
}
```
**Correct (stable subscription):**
```tsx
function useWindowEvent(event: string, handler: (e: Event) => void) {
const handlerRef = useRef(handler)
useEffect(() => {
handlerRef.current = handler
}, [handler])
useEffect(() => {
const listener = (e: Event) => handlerRef.current(e)
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}, [event])
}
```
**Alternative: use `useEffectEvent` if you're on latest React:**
```tsx
import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: () => void) {
const onEvent = useEffectEvent(handler)
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
}
```
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
@@ -0,0 +1,49 @@
---
title: useLatest for Stable Callback Refs
impact: LOW
impactDescription: prevents effect re-runs
tags: advanced, hooks, useLatest, refs, optimization
---
## useLatest for Stable Callback Refs
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
**Implementation:**
```typescript
function useLatest<T>(value: T) {
const ref = useRef(value)
useEffect(() => {
ref.current = value
}, [value])
return ref
}
```
**Incorrect (effect re-runs on every callback change):**
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
useEffect(() => {
const timeout = setTimeout(() => onSearch(query), 300)
return () => clearTimeout(timeout)
}, [query, onSearch])
}
```
**Correct (stable effect, fresh callback):**
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
const onSearchRef = useLatest(onSearch)
useEffect(() => {
const timeout = setTimeout(() => onSearchRef.current(query), 300)
return () => clearTimeout(timeout)
}, [query])
}
```
@@ -0,0 +1,38 @@
---
title: Prevent Waterfall Chains in API Routes
impact: CRITICAL
impactDescription: 2-10× improvement
tags: api-routes, server-actions, waterfalls, parallelization
---
## Prevent Waterfall Chains in API Routes
In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
**Incorrect (config waits for auth, data waits for both):**
```typescript
export async function GET(request: Request) {
const session = await auth()
const config = await fetchConfig()
const data = await fetchData(session.user.id)
return Response.json({ data, config })
}
```
**Correct (auth and config start immediately):**
```typescript
export async function GET(request: Request) {
const sessionPromise = auth()
const configPromise = fetchConfig()
const session = await sessionPromise
const [config, data] = await Promise.all([
configPromise,
fetchData(session.user.id)
])
return Response.json({ data, config })
}
```
For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
@@ -0,0 +1,80 @@
---
title: Defer Await Until Needed
impact: HIGH
impactDescription: avoids blocking unused code paths
tags: async, await, conditional, optimization
---
## Defer Await Until Needed
Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
**Incorrect (blocks both branches):**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
if (skipProcessing) {
// Returns immediately but still waited for userData
return { skipped: true }
}
// Only this branch uses userData
return processUserData(userData)
}
```
**Correct (only blocks when needed):**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
// Returns immediately without waiting
return { skipped: true }
}
// Fetch only when needed
const userData = await fetchUserData(userId)
return processUserData(userData)
}
```
**Another example (early return optimization):**
```typescript
// Incorrect: always fetches permissions
async function updateResource(resourceId: string, userId: string) {
const permissions = await fetchPermissions(userId)
const resource = await getResource(resourceId)
if (!resource) {
return { error: 'Not found' }
}
if (!permissions.canEdit) {
return { error: 'Forbidden' }
}
return await updateResourceData(resource, permissions)
}
// Correct: fetches only when needed
async function updateResource(resourceId: string, userId: string) {
const resource = await getResource(resourceId)
if (!resource) {
return { error: 'Not found' }
}
const permissions = await fetchPermissions(userId)
if (!permissions.canEdit) {
return { error: 'Forbidden' }
}
return await updateResourceData(resource, permissions)
}
```
This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
@@ -0,0 +1,36 @@
---
title: Dependency-Based Parallelization
impact: CRITICAL
impactDescription: 2-10× improvement
tags: async, parallelization, dependencies, better-all
---
## Dependency-Based Parallelization
For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
**Incorrect (profile waits for config unnecessarily):**
```typescript
const [user, config] = await Promise.all([
fetchUser(),
fetchConfig()
])
const profile = await fetchProfile(user.id)
```
**Correct (config and profile run in parallel):**
```typescript
import { all } from 'better-all'
const { user, config, profile } = await all({
async user() { return fetchUser() },
async config() { return fetchConfig() },
async profile() {
return fetchProfile((await this.$.user).id)
}
})
```
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
@@ -0,0 +1,28 @@
---
title: Promise.all() for Independent Operations
impact: CRITICAL
impactDescription: 2-10× improvement
tags: async, parallelization, promises, waterfalls
---
## Promise.all() for Independent Operations
When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
**Incorrect (sequential execution, 3 round trips):**
```typescript
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
```
**Correct (parallel execution, 1 round trip):**
```typescript
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
```
@@ -0,0 +1,99 @@
---
title: Strategic Suspense Boundaries
impact: HIGH
impactDescription: faster initial paint
tags: async, suspense, streaming, layout-shift
---
## Strategic Suspense Boundaries
Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
**Incorrect (wrapper blocked by data fetching):**
```tsx
async function Page() {
const data = await fetchData() // Blocks entire page
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<DataDisplay data={data} />
</div>
<div>Footer</div>
</div>
)
}
```
The entire layout waits for data even though only the middle section needs it.
**Correct (wrapper shows immediately, data streams in):**
```tsx
function Page() {
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
</div>
<div>Footer</div>
</div>
)
}
async function DataDisplay() {
const data = await fetchData() // Only blocks this component
return <div>{data.content}</div>
}
```
Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
**Alternative (share promise across components):**
```tsx
function Page() {
// Start fetch immediately, but don't await
const dataPromise = fetchData()
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<Suspense fallback={<Skeleton />}>
<DataDisplay dataPromise={dataPromise} />
<DataSummary dataPromise={dataPromise} />
</Suspense>
<div>Footer</div>
</div>
)
}
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Unwraps the promise
return <div>{data.content}</div>
}
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Reuses the same promise
return <div>{data.summary}</div>
}
```
Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
**When NOT to use this pattern:**
- Critical data needed for layout decisions (affects positioning)
- SEO-critical content above the fold
- Small, fast queries where suspense overhead isn't worth it
- When you want to avoid layout shift (loading → content jump)
**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
@@ -0,0 +1,59 @@
---
title: Avoid Barrel File Imports
impact: CRITICAL
impactDescription: 200-800ms import cost, slow builds
tags: bundle, imports, tree-shaking, barrel-files, performance
---
## Avoid Barrel File Imports
Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
**Incorrect (imports entire library):**
```tsx
import { Check, X, Menu } from 'lucide-react'
// Loads 1,583 modules, takes ~2.8s extra in dev
// Runtime cost: 200-800ms on every cold start
import { Button, TextField } from '@mui/material'
// Loads 2,225 modules, takes ~4.2s extra in dev
```
**Correct (imports only what you need):**
```tsx
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
import Menu from 'lucide-react/dist/esm/icons/menu'
// Loads only 3 modules (~2KB vs ~1MB)
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField'
// Loads only what you use
```
**Alternative (Next.js 13.5+):**
```js
// next.config.js - use optimizePackageImports
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react', '@mui/material']
}
}
// Then you can keep the ergonomic barrel imports:
import { Check, X, Menu } from 'lucide-react'
// Automatically transformed to direct imports at build time
```
Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
@@ -0,0 +1,33 @@
---
title: Conditional Module Loading
impact: HIGH
impactDescription: loads large data only when needed
tags: bundle, conditional-loading, lazy-loading
---
## Conditional Module Loading
Load large data or modules only when a feature is activated.
**Example (lazy-load animation frames):**
```tsx
function AnimationPlayer({ enabled }: { enabled: boolean }) {
const [frames, setFrames] = useState<Frame[] | null>(null)
const [loadError, setLoadError] = useState(false)
useEffect(() => {
if (enabled && !frames && !loadError && typeof window !== 'undefined') {
import('./animation-frames.js')
.then(mod => setFrames(mod.frames))
.catch(() => setLoadError(true))
}
}, [enabled, frames, loadError])
if (loadError) return <ErrorMessage />
if (!frames) return <Skeleton />
return <Canvas frames={frames} />
}
```
The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
@@ -0,0 +1,49 @@
---
title: Defer Non-Critical Third-Party Libraries
impact: MEDIUM
impactDescription: loads after hydration
tags: bundle, third-party, analytics, defer
---
## Defer Non-Critical Third-Party Libraries
Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
**Incorrect (blocks initial bundle):**
```tsx
import { Analytics } from '@vercel/analytics/react'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
)
}
```
**Correct (loads after hydration):**
```tsx
import dynamic from 'next/dynamic'
const Analytics = dynamic(
() => import('@vercel/analytics/react').then(m => m.Analytics),
{ ssr: false }
)
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
)
}
```
@@ -0,0 +1,35 @@
---
title: Dynamic Imports for Heavy Components
impact: CRITICAL
impactDescription: directly affects TTI and LCP
tags: bundle, dynamic-import, code-splitting, next-dynamic
---
## Dynamic Imports for Heavy Components
Use `next/dynamic` to lazy-load large components not needed on initial render.
**Incorrect (Monaco bundles with main chunk ~300KB):**
```tsx
import { MonacoEditor } from './monaco-editor'
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
}
```
**Correct (Monaco loads on demand):**
```tsx
import dynamic from 'next/dynamic'
const MonacoEditor = dynamic(
() => import('./monaco-editor').then(m => m.MonacoEditor),
{ ssr: false }
)
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
}
```
@@ -0,0 +1,50 @@
---
title: Preload Based on User Intent
impact: MEDIUM
impactDescription: reduces perceived latency
tags: bundle, preload, user-intent, hover
---
## Preload Based on User Intent
Preload heavy bundles before they're needed to reduce perceived latency.
**Example (preload on hover/focus):**
```tsx
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== 'undefined') {
void import('./monaco-editor')
}
}
return (
<button
onMouseEnter={preload}
onFocus={preload}
onClick={onClick}
>
Open Editor
</button>
)
}
```
**Example (preload when feature flag is enabled):**
```tsx
function FlagsProvider({ children, flags }: Props) {
useEffect(() => {
if (flags.editorEnabled && typeof window !== 'undefined') {
void import('./monaco-editor').then(mod => mod.init())
}
}, [flags.editorEnabled])
return <FlagsContext.Provider value={flags}>
{children}
</FlagsContext.Provider>
}
```
The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
@@ -0,0 +1,74 @@
---
title: Deduplicate Global Event Listeners
impact: LOW
impactDescription: single listener for N components
tags: client, swr, event-listeners, subscription
---
## Deduplicate Global Event Listeners
Use `useSWRSubscription()` to share global event listeners across component instances.
**Incorrect (N instances = N listeners):**
```tsx
function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.key === key) {
callback()
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [key, callback])
}
```
When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
**Correct (N instances = 1 listener):**
```tsx
import useSWRSubscription from 'swr/subscription'
// Module-level Map to track callbacks per key
const keyCallbacks = new Map<string, Set<() => void>>()
function useKeyboardShortcut(key: string, callback: () => void) {
// Register this callback in the Map
useEffect(() => {
if (!keyCallbacks.has(key)) {
keyCallbacks.set(key, new Set())
}
keyCallbacks.get(key)!.add(callback)
return () => {
const set = keyCallbacks.get(key)
if (set) {
set.delete(callback)
if (set.size === 0) {
keyCallbacks.delete(key)
}
}
}
}, [key, callback])
useSWRSubscription('global-keydown', () => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && keyCallbacks.has(e.key)) {
keyCallbacks.get(e.key)!.forEach(cb => cb())
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
})
}
function Profile() {
// Multiple shortcuts will share the same listener
useKeyboardShortcut('p', () => { /* ... */ })
useKeyboardShortcut('k', () => { /* ... */ })
// ...
}
```
@@ -0,0 +1,56 @@
---
title: Use SWR for Automatic Deduplication
impact: MEDIUM-HIGH
impactDescription: automatic deduplication
tags: client, swr, deduplication, data-fetching
---
## Use SWR for Automatic Deduplication
SWR enables request deduplication, caching, and revalidation across component instances.
**Incorrect (no deduplication, each instance fetches):**
```tsx
function UserList() {
const [users, setUsers] = useState([])
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(setUsers)
}, [])
}
```
**Correct (multiple instances share one request):**
```tsx
import useSWR from 'swr'
function UserList() {
const { data: users } = useSWR('/api/users', fetcher)
}
```
**For immutable data:**
```tsx
import { useImmutableSWR } from '@/lib/swr'
function StaticContent() {
const { data } = useImmutableSWR('/api/config', fetcher)
}
```
**For mutations:**
```tsx
import { useSWRMutation } from 'swr/mutation'
function UpdateButton() {
const { trigger } = useSWRMutation('/api/user', updateUser)
return <button onClick={() => trigger()}>Update</button>
}
```
Reference: [https://swr.vercel.app](https://swr.vercel.app)
@@ -0,0 +1,82 @@
---
title: Batch DOM CSS Changes
impact: MEDIUM
impactDescription: reduces reflows/repaints
tags: javascript, dom, css, performance, reflow
---
## Batch DOM CSS Changes
Avoid changing styles one property at a time. Group multiple CSS changes together via classes or `cssText` to minimize browser reflows.
**Incorrect (multiple reflows):**
```typescript
function updateElementStyles(element: HTMLElement) {
// Each line triggers a reflow
element.style.width = '100px'
element.style.height = '200px'
element.style.backgroundColor = 'blue'
element.style.border = '1px solid black'
}
```
**Correct (add class - single reflow):**
```typescript
// CSS file
.highlighted-box {
width: 100px;
height: 200px;
background-color: blue;
border: 1px solid black;
}
// JavaScript
function updateElementStyles(element: HTMLElement) {
element.classList.add('highlighted-box')
}
```
**Correct (change cssText - single reflow):**
```typescript
function updateElementStyles(element: HTMLElement) {
element.style.cssText = `
width: 100px;
height: 200px;
background-color: blue;
border: 1px solid black;
`
}
```
**React example:**
```tsx
// Incorrect: changing styles one by one
function Box({ isHighlighted }: { isHighlighted: boolean }) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (ref.current && isHighlighted) {
ref.current.style.width = '100px'
ref.current.style.height = '200px'
ref.current.style.backgroundColor = 'blue'
}
}, [isHighlighted])
return <div ref={ref}>Content</div>
}
// Correct: toggle class
function Box({ isHighlighted }: { isHighlighted: boolean }) {
return (
<div className={isHighlighted ? 'highlighted-box' : ''}>
Content
</div>
)
}
```
Prefer CSS classes over inline styles when possible. Classes are cached by the browser and provide better separation of concerns.
@@ -0,0 +1,80 @@
---
title: Cache Repeated Function Calls
impact: MEDIUM
impactDescription: avoid redundant computation
tags: javascript, cache, memoization, performance
---
## Cache Repeated Function Calls
Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
**Incorrect (redundant computation):**
```typescript
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
// slugify() called 100+ times for same project names
const slug = slugify(project.name)
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
)
}
```
**Correct (cached results):**
```typescript
// Module-level cache
const slugifyCache = new Map<string, string>()
function cachedSlugify(text: string): string {
if (slugifyCache.has(text)) {
return slugifyCache.get(text)!
}
const result = slugify(text)
slugifyCache.set(text, result)
return result
}
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
// Computed only once per unique project name
const slug = cachedSlugify(project.name)
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
)
}
```
**Simpler pattern for single-value functions:**
```typescript
let isLoggedInCache: boolean | null = null
function isLoggedIn(): boolean {
if (isLoggedInCache !== null) {
return isLoggedInCache
}
isLoggedInCache = document.cookie.includes('auth=')
return isLoggedInCache
}
// Clear cache when auth changes
function onAuthChange() {
isLoggedInCache = null
}
```
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
@@ -0,0 +1,28 @@
---
title: Cache Property Access in Loops
impact: LOW-MEDIUM
impactDescription: reduces lookups
tags: javascript, loops, optimization, caching
---
## Cache Property Access in Loops
Cache object property lookups in hot paths.
**Incorrect (3 lookups × N iterations):**
```typescript
for (let i = 0; i < arr.length; i++) {
process(obj.config.settings.value)
}
```
**Correct (1 lookup total):**
```typescript
const value = obj.config.settings.value
const len = arr.length
for (let i = 0; i < len; i++) {
process(value)
}
```
@@ -0,0 +1,70 @@
---
title: Cache Storage API Calls
impact: LOW-MEDIUM
impactDescription: reduces expensive I/O
tags: javascript, localStorage, storage, caching, performance
---
## Cache Storage API Calls
`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
**Incorrect (reads storage on every call):**
```typescript
function getTheme() {
return localStorage.getItem('theme') ?? 'light'
}
// Called 10 times = 10 storage reads
```
**Correct (Map cache):**
```typescript
const storageCache = new Map<string, string | null>()
function getLocalStorage(key: string) {
if (!storageCache.has(key)) {
storageCache.set(key, localStorage.getItem(key))
}
return storageCache.get(key)
}
function setLocalStorage(key: string, value: string) {
localStorage.setItem(key, value)
storageCache.set(key, value) // keep cache in sync
}
```
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
**Cookie caching:**
```typescript
let cookieCache: Record<string, string> | null = null
function getCookie(name: string) {
if (!cookieCache) {
cookieCache = Object.fromEntries(
document.cookie.split('; ').map(c => c.split('='))
)
}
return cookieCache[name]
}
```
**Important (invalidate on external changes):**
If storage can change externally (another tab, server-set cookies), invalidate cache:
```typescript
window.addEventListener('storage', (e) => {
if (e.key) storageCache.delete(e.key)
})
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
storageCache.clear()
}
})
```
@@ -0,0 +1,32 @@
---
title: Combine Multiple Array Iterations
impact: LOW-MEDIUM
impactDescription: reduces iterations
tags: javascript, arrays, loops, performance
---
## Combine Multiple Array Iterations
Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
**Incorrect (3 iterations):**
```typescript
const admins = users.filter(u => u.isAdmin)
const testers = users.filter(u => u.isTester)
const inactive = users.filter(u => !u.isActive)
```
**Correct (1 iteration):**
```typescript
const admins: User[] = []
const testers: User[] = []
const inactive: User[] = []
for (const user of users) {
if (user.isAdmin) admins.push(user)
if (user.isTester) testers.push(user)
if (!user.isActive) inactive.push(user)
}
```
@@ -0,0 +1,50 @@
---
title: Early Return from Functions
impact: LOW-MEDIUM
impactDescription: avoids unnecessary computation
tags: javascript, functions, optimization, early-return
---
## Early Return from Functions
Return early when result is determined to skip unnecessary processing.
**Incorrect (processes all items even after finding answer):**
```typescript
function validateUsers(users: User[]) {
let hasError = false
let errorMessage = ''
for (const user of users) {
if (!user.email) {
hasError = true
errorMessage = 'Email required'
}
if (!user.name) {
hasError = true
errorMessage = 'Name required'
}
// Continues checking all users even after error found
}
return hasError ? { valid: false, error: errorMessage } : { valid: true }
}
```
**Correct (returns immediately on first error):**
```typescript
function validateUsers(users: User[]) {
for (const user of users) {
if (!user.email) {
return { valid: false, error: 'Email required' }
}
if (!user.name) {
return { valid: false, error: 'Name required' }
}
}
return { valid: true }
}
```
@@ -0,0 +1,45 @@
---
title: Hoist RegExp Creation
impact: LOW-MEDIUM
impactDescription: avoids recreation
tags: javascript, regexp, optimization, memoization
---
## Hoist RegExp Creation
Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
**Incorrect (new RegExp every render):**
```tsx
function Highlighter({ text, query }: Props) {
const regex = new RegExp(`(${query})`, 'gi')
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
```
**Correct (memoize or hoist):**
```tsx
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
function Highlighter({ text, query }: Props) {
const regex = useMemo(
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
[query]
)
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
```
**Warning (global regex has mutable state):**
Global regex (`/g`) has mutable `lastIndex` state:
```typescript
const regex = /foo/g
regex.test('foo') // true, lastIndex = 3
regex.test('foo') // false, lastIndex = 0
```
@@ -0,0 +1,37 @@
---
title: Build Index Maps for Repeated Lookups
impact: LOW-MEDIUM
impactDescription: 1M ops to 2K ops
tags: javascript, map, indexing, optimization, performance
---
## Build Index Maps for Repeated Lookups
Multiple `.find()` calls by the same key should use a Map.
**Incorrect (O(n) per lookup):**
```typescript
function processOrders(orders: Order[], users: User[]) {
return orders.map(order => ({
...order,
user: users.find(u => u.id === order.userId)
}))
}
```
**Correct (O(1) per lookup):**
```typescript
function processOrders(orders: Order[], users: User[]) {
const userById = new Map(users.map(u => [u.id, u]))
return orders.map(order => ({
...order,
user: userById.get(order.userId)
}))
}
```
Build map once (O(n)), then all lookups are O(1).
For 1000 orders × 1000 users: 1M ops → 2K ops.
@@ -0,0 +1,49 @@
---
title: Early Length Check for Array Comparisons
impact: MEDIUM-HIGH
impactDescription: avoids expensive operations when lengths differ
tags: javascript, arrays, performance, optimization, comparison
---
## Early Length Check for Array Comparisons
When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
**Incorrect (always runs expensive comparison):**
```typescript
function hasChanges(current: string[], original: string[]) {
// Always sorts and joins, even when lengths differ
return current.sort().join() !== original.sort().join()
}
```
Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.
**Correct (O(1) length check first):**
```typescript
function hasChanges(current: string[], original: string[]) {
// Early return if lengths differ
if (current.length !== original.length) {
return true
}
// Only sort/join when lengths match
const currentSorted = current.toSorted()
const originalSorted = original.toSorted()
for (let i = 0; i < currentSorted.length; i++) {
if (currentSorted[i] !== originalSorted[i]) {
return true
}
}
return false
}
```
This new approach is more efficient because:
- It avoids the overhead of sorting and joining the arrays when lengths differ
- It avoids consuming memory for the joined strings (especially important for large arrays)
- It avoids mutating the original arrays
- It returns early when a difference is found
@@ -0,0 +1,82 @@
---
title: Use Loop for Min/Max Instead of Sort
impact: LOW
impactDescription: O(n) instead of O(n log n)
tags: javascript, arrays, performance, sorting, algorithms
---
## Use Loop for Min/Max Instead of Sort
Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
**Incorrect (O(n log n) - sort to find latest):**
```typescript
interface Project {
id: string
name: string
updatedAt: number
}
function getLatestProject(projects: Project[]) {
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
return sorted[0]
}
```
Sorts the entire array just to find the maximum value.
**Incorrect (O(n log n) - sort for oldest and newest):**
```typescript
function getOldestAndNewest(projects: Project[]) {
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
}
```
Still sorts unnecessarily when only min/max are needed.
**Correct (O(n) - single loop):**
```typescript
function getLatestProject(projects: Project[]) {
if (projects.length === 0) return null
let latest = projects[0]
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt > latest.updatedAt) {
latest = projects[i]
}
}
return latest
}
function getOldestAndNewest(projects: Project[]) {
if (projects.length === 0) return { oldest: null, newest: null }
let oldest = projects[0]
let newest = projects[0]
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
}
return { oldest, newest }
}
```
Single pass through the array, no copying, no sorting.
**Alternative (Math.min/Math.max for small arrays):**
```typescript
const numbers = [5, 2, 8, 1, 9]
const min = Math.min(...numbers)
const max = Math.max(...numbers)
```
This works for small arrays but can be slower for very large arrays due to spread operator limitations. Use the loop approach for reliability.
@@ -0,0 +1,24 @@
---
title: Use Set/Map for O(1) Lookups
impact: LOW-MEDIUM
impactDescription: O(n) to O(1)
tags: javascript, set, map, data-structures, performance
---
## Use Set/Map for O(1) Lookups
Convert arrays to Set/Map for repeated membership checks.
**Incorrect (O(n) per check):**
```typescript
const allowedIds = ['a', 'b', 'c', ...]
items.filter(item => allowedIds.includes(item.id))
```
**Correct (O(1) per check):**
```typescript
const allowedIds = new Set(['a', 'b', 'c', ...])
items.filter(item => allowedIds.has(item.id))
```
@@ -0,0 +1,57 @@
---
title: Use toSorted() Instead of sort() for Immutability
impact: MEDIUM-HIGH
impactDescription: prevents mutation bugs in React state
tags: javascript, arrays, immutability, react, state, mutation
---
## Use toSorted() Instead of sort() for Immutability
`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.
**Incorrect (mutates original array):**
```typescript
function UserList({ users }: { users: User[] }) {
// Mutates the users prop array!
const sorted = useMemo(
() => users.sort((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
}
```
**Correct (creates new array):**
```typescript
function UserList({ users }: { users: User[] }) {
// Creates new sorted array, original unchanged
const sorted = useMemo(
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
}
```
**Why this matters in React:**
1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
**Browser support (fallback for older browsers):**
`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
```typescript
// Fallback for older browsers
const sorted = [...items].sort((a, b) => a.value - b.value)
```
**Other immutable array methods:**
- `.toSorted()` - immutable sort
- `.toReversed()` - immutable reverse
- `.toSpliced()` - immutable splice
- `.with()` - immutable element replacement
@@ -0,0 +1,26 @@
---
title: Use Activity Component for Show/Hide
impact: MEDIUM
impactDescription: preserves state/DOM
tags: rendering, activity, visibility, state-preservation
---
## Use Activity Component for Show/Hide
Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
**Usage:**
```tsx
import { Activity } from 'react'
function Dropdown({ isOpen }: Props) {
return (
<Activity mode={isOpen ? 'visible' : 'hidden'}>
<ExpensiveMenu />
</Activity>
)
}
```
Avoids expensive re-renders and state loss.
@@ -0,0 +1,47 @@
---
title: Animate SVG Wrapper Instead of SVG Element
impact: LOW
impactDescription: enables hardware acceleration
tags: rendering, svg, css, animation, performance
---
## Animate SVG Wrapper Instead of SVG Element
Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
**Incorrect (animating SVG directly - no hardware acceleration):**
```tsx
function LoadingSpinner() {
return (
<svg
className="animate-spin"
width="24"
height="24"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
)
}
```
**Correct (animating wrapper div - hardware accelerated):**
```tsx
function LoadingSpinner() {
return (
<div className="animate-spin">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
</div>
)
}
```
This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
@@ -0,0 +1,40 @@
---
title: Use Explicit Conditional Rendering
impact: LOW
impactDescription: prevents rendering 0 or NaN
tags: rendering, conditional, jsx, falsy-values
---
## Use Explicit Conditional Rendering
Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
**Incorrect (renders "0" when count is 0):**
```tsx
function Badge({ count }: { count: number }) {
return (
<div>
{count && <span className="badge">{count}</span>}
</div>
)
}
// When count = 0, renders: <div>0</div>
// When count = 5, renders: <div><span class="badge">5</span></div>
```
**Correct (renders nothing when count is 0):**
```tsx
function Badge({ count }: { count: number }) {
return (
<div>
{count > 0 ? <span className="badge">{count}</span> : null}
</div>
)
}
// When count = 0, renders: <div></div>
// When count = 5, renders: <div><span class="badge">5</span></div>
```
@@ -0,0 +1,38 @@
---
title: CSS content-visibility for Long Lists
impact: HIGH
impactDescription: faster initial render
tags: rendering, css, content-visibility, long-lists
---
## CSS content-visibility for Long Lists
Apply `content-visibility: auto` to defer off-screen rendering.
**CSS:**
```css
.message-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
```
**Example:**
```tsx
function MessageList({ messages }: { messages: Message[] }) {
return (
<div className="overflow-y-auto h-screen">
{messages.map(msg => (
<div key={msg.id} className="message-item">
<Avatar user={msg.author} />
<div>{msg.content}</div>
</div>
))}
</div>
)
}
```
For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
@@ -0,0 +1,46 @@
---
title: Hoist Static JSX Elements
impact: LOW
impactDescription: avoids re-creation
tags: rendering, jsx, static, optimization
---
## Hoist Static JSX Elements
Extract static JSX outside components to avoid re-creation.
**Incorrect (recreates element every render):**
```tsx
function LoadingSkeleton() {
return <div className="animate-pulse h-20 bg-gray-200" />
}
function Container() {
return (
<div>
{loading && <LoadingSkeleton />}
</div>
)
}
```
**Correct (reuses same element):**
```tsx
const loadingSkeleton = (
<div className="animate-pulse h-20 bg-gray-200" />
)
function Container() {
return (
<div>
{loading && loadingSkeleton}
</div>
)
}
```
This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
@@ -0,0 +1,82 @@
---
title: Prevent Hydration Mismatch Without Flickering
impact: MEDIUM
impactDescription: avoids visual flicker and hydration errors
tags: rendering, ssr, hydration, localStorage, flicker
---
## Prevent Hydration Mismatch Without Flickering
When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
**Incorrect (breaks SSR):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
// localStorage is not available on server - throws error
const theme = localStorage.getItem('theme') || 'light'
return (
<div className={theme}>
{children}
</div>
)
}
```
Server-side rendering will fail because `localStorage` is undefined.
**Incorrect (visual flickering):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState('light')
useEffect(() => {
// Runs after hydration - causes visible flash
const stored = localStorage.getItem('theme')
if (stored) {
setTheme(stored)
}
}, [])
return (
<div className={theme}>
{children}
</div>
)
}
```
Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
**Correct (no flicker, no hydration mismatch):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
return (
<>
<div id="theme-wrapper">
{children}
</div>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var theme = localStorage.getItem('theme') || 'light';
var el = document.getElementById('theme-wrapper');
if (el) el.className = theme;
} catch (e) {}
})();
`,
}}
/>
</>
)
}
```
The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
@@ -0,0 +1,28 @@
---
title: Optimize SVG Precision
impact: LOW
impactDescription: reduces file size
tags: rendering, svg, optimization, svgo
---
## Optimize SVG Precision
Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
**Incorrect (excessive precision):**
```svg
<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
```
**Correct (1 decimal place):**
```svg
<path d="M 10.3 20.8 L 30.9 40.2" />
```
**Automate with SVGO:**
```bash
npx svgo --precision=1 --multipass icon.svg
```
@@ -0,0 +1,39 @@
---
title: Defer State Reads to Usage Point
impact: MEDIUM
impactDescription: avoids unnecessary subscriptions
tags: rerender, searchParams, localStorage, optimization
---
## Defer State Reads to Usage Point
Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
**Incorrect (subscribes to all searchParams changes):**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const searchParams = useSearchParams()
const handleShare = () => {
const ref = searchParams.get('ref')
shareChat(chatId, { ref })
}
return <button onClick={handleShare}>Share</button>
}
```
**Correct (reads on demand, no subscription):**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
const ref = params.get('ref')
shareChat(chatId, { ref })
}
return <button onClick={handleShare}>Share</button>
}
```
@@ -0,0 +1,45 @@
---
title: Narrow Effect Dependencies
impact: LOW
impactDescription: minimizes effect re-runs
tags: rerender, useEffect, dependencies, optimization
---
## Narrow Effect Dependencies
Specify primitive dependencies instead of objects to minimize effect re-runs.
**Incorrect (re-runs on any user field change):**
```tsx
useEffect(() => {
fetchUserDetails(user.id)
}, [user])
```
**Correct (re-runs only when id changes):**
```tsx
useEffect(() => {
fetchUserDetails(user.id)
}, [user.id])
```
**For derived state, compute outside effect:**
```tsx
// Incorrect: runs on width=767, 766, 765...
useEffect(() => {
if (width < 768) {
enableMobileMode()
}
}, [width])
// Correct: runs only on boolean transition
const isMobile = width < 768
useEffect(() => {
if (isMobile) {
enableMobileMode()
}
}, [isMobile])
```
@@ -0,0 +1,29 @@
---
title: Subscribe to Derived State
impact: MEDIUM
impactDescription: reduces re-render frequency
tags: rerender, derived-state, media-query, optimization
---
## Subscribe to Derived State
Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
**Incorrect (re-renders on every pixel change):**
```tsx
function Sidebar() {
const width = useWindowWidth() // updates continuously
const isMobile = width < 768
return <nav className={isMobile ? 'mobile' : 'desktop'}></nav>
}
```
**Correct (re-renders only when boolean changes):**
```tsx
function Sidebar() {
const isMobile = useMediaQuery('(max-width: 767px)')
return <nav className={isMobile ? 'mobile' : 'desktop'}></nav>
}
```
@@ -0,0 +1,74 @@
---
title: Use Functional setState Updates
impact: MEDIUM
impactDescription: prevents stale closures and unnecessary callback recreations
tags: react, hooks, useState, useCallback, callbacks, closures
---
## Use Functional setState Updates
When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
**Incorrect (requires state as dependency):**
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems)
// Callback must depend on items, recreated on every items change
const addItems = useCallback((newItems: Item[]) => {
setItems([...items, ...newItems])
}, [items]) // ❌ items dependency causes recreations
// Risk of stale closure if dependency is forgotten
const removeItem = useCallback((id: string) => {
setItems(items.filter(item => item.id !== id))
}, []) // ❌ Missing items dependency - will use stale items!
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
}
```
The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
**Correct (stable callbacks, no stale closures):**
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems)
// Stable callback, never recreated
const addItems = useCallback((newItems: Item[]) => {
setItems(curr => [...curr, ...newItems])
}, []) // ✅ No dependencies needed
// Always uses latest state, no stale closure risk
const removeItem = useCallback((id: string) => {
setItems(curr => curr.filter(item => item.id !== id))
}, []) // ✅ Safe and stable
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
}
```
**Benefits:**
1. **Stable callback references** - Callbacks don't need to be recreated when state changes
2. **No stale closures** - Always operates on the latest state value
3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
4. **Prevents bugs** - Eliminates the most common source of React closure bugs
**When to use functional updates:**
- Any setState that depends on the current state value
- Inside useCallback/useMemo when state is needed
- Event handlers that reference state
- Async operations that update state
**When direct updates are fine:**
- Setting state to a static value: `setCount(0)`
- Setting state from props/arguments only: `setName(newName)`
- State doesn't depend on previous value
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
@@ -0,0 +1,58 @@
---
title: Use Lazy State Initialization
impact: MEDIUM
impactDescription: wasted computation on every render
tags: react, hooks, useState, performance, initialization
---
## Use Lazy State Initialization
Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
**Incorrect (runs on every render):**
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs on EVERY render, even after initialization
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
const [query, setQuery] = useState('')
// When query changes, buildSearchIndex runs again unnecessarily
return <SearchResults index={searchIndex} query={query} />
}
function UserProfile() {
// JSON.parse runs on every render
const [settings, setSettings] = useState(
JSON.parse(localStorage.getItem('settings') || '{}')
)
return <SettingsForm settings={settings} onChange={setSettings} />
}
```
**Correct (runs only once):**
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs ONLY on initial render
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
const [query, setQuery] = useState('')
return <SearchResults index={searchIndex} query={query} />
}
function UserProfile() {
// JSON.parse runs only on initial render
const [settings, setSettings] = useState(() => {
const stored = localStorage.getItem('settings')
return stored ? JSON.parse(stored) : {}
})
return <SettingsForm settings={settings} onChange={setSettings} />
}
```
Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
@@ -0,0 +1,44 @@
---
title: Extract to Memoized Components
impact: MEDIUM
impactDescription: enables early returns
tags: rerender, memo, useMemo, optimization
---
## Extract to Memoized Components
Extract expensive work into memoized components to enable early returns before computation.
**Incorrect (computes avatar even when loading):**
```tsx
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => {
const id = computeAvatarId(user)
return <Avatar id={id} />
}, [user])
if (loading) return <Skeleton />
return <div>{avatar}</div>
}
```
**Correct (skips computation when loading):**
```tsx
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user])
return <Avatar id={id} />
})
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton />
return (
<div>
<UserAvatar user={user} />
</div>
)
}
```
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
@@ -0,0 +1,40 @@
---
title: Use Transitions for Non-Urgent Updates
impact: MEDIUM
impactDescription: maintains UI responsiveness
tags: rerender, transitions, startTransition, performance
---
## Use Transitions for Non-Urgent Updates
Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
**Incorrect (blocks UI on every scroll):**
```tsx
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
const handler = () => setScrollY(window.scrollY)
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
}
```
**Correct (non-blocking updates):**
```tsx
import { startTransition } from 'react'
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY))
}
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
}
```
@@ -0,0 +1,72 @@
---
title: Use after() for Non-Blocking Operations
impact: MEDIUM
impactDescription: faster response times
tags: server, async, logging, analytics, side-effects
---
## Use after() for Non-Blocking Operations
Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
**Incorrect (blocks response):**
```tsx
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
// Logging blocks the response
const userAgent = request.headers.get('user-agent') || 'unknown'
await logUserAction({ userAgent })
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
```
**Correct (non-blocking):**
```tsx
import { after } from 'next/server'
import { headers } from 'next/headers'
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
// Log after response is sent
after(async () => {
const userAgent = (await headers()).get('user-agent') || 'unknown'
await logUserAction({ userAgent })
})
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
```
The response is sent immediately while logging happens in the background.
**Common use cases:**
- Analytics tracking
- Audit logging
- Sending notifications
- Cache invalidation
- Cleanup tasks
**Important notes:**
- `after()` runs even if the response fails or redirects
- Works in Server Actions, Route Handlers, and Server Components
Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
@@ -0,0 +1,41 @@
---
title: Cross-Request LRU Caching
impact: HIGH
impactDescription: caches across requests
tags: server, cache, lru, cross-request
---
## Cross-Request LRU Caching
`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
**Implementation:**
```typescript
import { LRUCache } from 'lru-cache'
const cache = new LRUCache<string, any>({
max: 1000,
ttl: 5 * 60 * 1000 // 5 minutes
})
export async function getUser(id: string) {
const cached = cache.get(id)
if (cached) return cached
const user = await db.user.findUnique({ where: { id } })
cache.set(id, user)
return user
}
// Request 1: DB query, result cached
// Request 2: cache hit, no DB query
```
Use when sequential user actions hit multiple endpoints needing the same data within seconds.
**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
@@ -0,0 +1,26 @@
---
title: Per-Request Deduplication with React.cache()
impact: MEDIUM
impactDescription: deduplicates within request
tags: server, cache, react-cache, deduplication
---
## Per-Request Deduplication with React.cache()
Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
**Usage:**
```typescript
import { cache } from 'react'
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
return await db.user.findUnique({
where: { id: session.user.id }
})
})
```
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.

Some files were not shown because too many files have changed in this diff Show More