This document defines the coding conventions and best practices for all implementations in the AI Product OS.
All engineering agents must follow these standards when generating code.
- Required: All new code must be written in TypeScript, not JavaScript
- Use strict mode (
"strict": truein tsconfig.json) - Avoid
anytypes - use proper type definitions orunknownwith type guards - Export types alongside implementations for reusability
- Use App Router (
app/directory) over Pages Router for all new projects - Organize by feature:
app/[feature]/page.tsx,app/api/[resource]/route.ts - Server Components by default, Client Components only when necessary (
"use client") - API Routes must use modern
NextResponseandNextRequesttypes
apps/[project-name]/
src/
app/ # Next.js App Router
api/ # API routes
[feature]/ # Feature-based pages
layout.tsx # Root layout
components/ # Reusable UI components
lib/ # Utilities, clients, helpers
public/ # Static assets
schema.prisma # Prisma schema
package.json
README.md
- Files:
kebab-case.ts,PascalCase.tsx(for components) - Components:
PascalCase(e.g.,TaskBoard,PostHogProvider) - Functions:
camelCase(e.g.,fetchTasks,markDone) - Constants:
SCREAMING_SNAKE_CASE(e.g.,PM_CATEGORIES,API_TIMEOUT) - Types/Interfaces:
PascalCase(e.g.,Task,Category)
- Always validate inputs: Check for
null,undefined, empty strings, type mismatches - Enforce limits early: Prevent abuse by validating length, count, size at the entry point
- Return descriptive errors: Use 400 for client errors with clear messages
// GOOD
if (!taskText || typeof taskText !== 'string' || taskText.trim().length === 0) {
return NextResponse.json({ error: 'Task text is required' }, { status: 400 });
}
if (taskText.length > 500) {
return NextResponse.json({ error: 'Task text too long (max 500 chars)' }, { status: 400 });
}- Always use
.take()(Prisma): Never fetch unbounded lists - Index primary queries: Ensure columns used in where/orderBy are indexed
- Use batch operations: Prefer
createManyorupdateManyover N individual queries in loops
// GOOD - Limited query with Prisma
const tasks = await prisma.task.findMany({
orderBy: { createdAt: 'desc' },
take: 100,
});
// BAD - Unbounded query
const tasks = await prisma.task.findMany();- Never fire-and-forget: All async operations in API routes MUST be
awaited before returning - No background promises: Serverless environments (Vercel, AWS Lambda) suspend execution immediately after HTTP response
// GOOD
await sendWhatsAppMessage(userId, message);
return NextResponse.json({ success: true });
// BAD - Message will be dropped in production
sendWhatsAppMessage(userId, message); // Fire-and-forget
return NextResponse.json({ success: true });- Use
Promise.all()orPromise.allSettled()for independent parallel operations - Avoid sequential
awaitin loops when operations can run concurrently
// GOOD - Concurrent execution
const results = await Promise.allSettled(users.map((user) => processUser(user)));
// BAD - Sequential execution (slow)
for (const user of users) {
await processUser(user);
}- Classify errors: Distinguish between transient (503, rate limit) and permanent (404, 401)
- Implement fallbacks: Never lose user data due to third-party failures
- Log comprehensively: Use structured logging with context
try {
const result = JSON.parse(aiResponse.text);
} catch (e) {
console.error('Failed to parse AI JSON:', aiResponse.text);
// Apply fallback to prevent data loss
result = {
category: 'ops',
priority: 'medium',
title: `[Review Needed] ${input.substring(0, 30)}...`,
};
}- Sanitize before parsing: Strip markdown codeblocks from LLM outputs
- Validate structure: Ensure required fields exist and match expected types
- Provide graceful degradation: Save raw input if AI processing fails
// GOOD - Defensive parsing
const cleanText = resultText
.replace(/```json\n?/g, '')
.replace(/```\n?/g, '')
.trim();
const result = JSON.parse(cleanText);
if (!VALID_CATEGORIES.includes(result.category)) {
console.error('Invalid AI categorization:', result);
isFallback = true;
}Never use a silent catch block on clipboard copy. If the user triggers a copy action during a live workflow, silent failure is equivalent to a broken product.
// GOOD - Clipboard with fallback + error state
async function copyToClipboard(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
try {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
return true;
} catch {
return false;
}
}
}
// In component: show inline error ("Copy failed — please select manually") if copyToClipboard returns false// Added: 2026-03-19 — SMB Feature Bundling Engine
- Input limits: Max 500 characters for user text inputs
- Pagination: Default page size of 100, never exceed 1000
- API timeouts: 10s for AI calls, 5s for database queries
- Cron duration: Max 4 minutes for serverless cron jobs (stay under platform limits)
- External API loops: MUST have both a page limit AND a temporal bound
- Example:
max 5 pagesANDnewer_than:30dfor Gmail syncing - Failure limits: Implement retry counters or dead-letter queues to prevent infinite poison-pill loops
- Never commit secrets: Use
.env.localfor local dev, platform secrets for production - Prefix public vars:
NEXT_PUBLIC_*for client-safe vars only - Encrypt sensitive data: Use AES-256-GCM for OAuth tokens, API keys in database
- Enable on all user tables: Neon tables should use RLS if required, but prefer server-enforced ownership in the API layer
- Default deny: Start with no access, explicitly grant permissions
- Use auth.uid(): Tie policies to authenticated user ID
- Happy path: Standard valid input
- Edge cases: Empty strings, zero values, maximum lengths
- Invalid inputs: Wrong types, special characters, malformed JSON
- Network failures: API timeouts, 500 errors, rate limits
- Concurrent operations: Race conditions, optimistic UI updates
- Test all error states (not just success paths)
- Verify data persistence (especially for optimistic UI)
- Check boundary values (0, null, max length)
- Validate media handling (images, audio, non-text)
- Primary: Bun (for speed and built-in tooling)
- Use
bun installfor dependency management - Lock versions in
package.jsonfor production stability
- Frontend: React 19+, Next.js 16+, Tailwind CSS 4+
- Database: Neon (Postgres) + Prisma ORM
- AI: Google Gemini (
@google/genai), OpenAI, or Anthropic - Analytics: PostHog (
posthog-js+posthog-node) - Error Tracking: Sentry (
@sentry/nextjs) — mandatory in all apps - UI Libraries: Framer Motion, Lucide Icons, Radix UI
Before writing posthog.ts, prisma.ts, or error handling from scratch, copy from /libs/shared/:
| Template | Copy to | Purpose |
|---|---|---|
libs/shared/posthog.ts |
src/lib/posthog.ts |
PostHog client + server setup, captureServerEvent() |
libs/shared/prisma.ts |
src/lib/prisma.ts |
Prisma client singleton, specialized Neon serverless setup |
libs/shared/error-handler.ts |
src/lib/error-handler.ts |
API errors, AI parsing, timeout wrapper, auth validation |
- Complex logic: Explain non-obvious algorithms or business rules
- Workarounds: Document why something is done a specific way
- TODOs: Mark incomplete or temporary code with
// TODO: [reason]
- Obvious code: Don't comment what the code already says
- Over-documentation: Code should be self-explanatory through naming
// GOOD - Explains WHY
// Strip markdown codeblocks because Gemini sometimes wraps JSON in ```json blocks
const cleanText = resultText.replace(/```json\n?/g, '');
// BAD - Explains WHAT (code already says this)
// Parse the JSON
const result = JSON.parse(cleanText);Sentry is a mandatory dependency for all apps. It is not optional for MVP.
Setup:
bun add @sentry/nextjsRequired files:
apps/[project]/
sentry.client.config.ts # Browser error capture
sentry.server.config.ts # Server/edge error capture
next.config.ts # Must use withSentryConfig()
Minimum configuration:
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
environment: process.env.NODE_ENV,
});Required in every try/catch block in API routes:
try {
// ...
} catch (e) {
Sentry.captureException(e);
console.error('[route-name] failed:', e);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}Env vars (add to .env.local.example):
NEXT_PUBLIC_SENTRY_DSN=your-dsn-here
SENTRY_AUTH_TOKEN=your-auth-token-here
- Use conventional commits:
feat:,fix:,docs:,chore: - Keep first line under 72 characters
- Add body for complex changes explaining WHY
The following rules are extracted from actual production issues:
- Unbounded pagination loops MUST have page limits AND date bounds
- AI summarization MUST use full payloads, not snippets
- Cron jobs MUST use fan-out architecture for per-user processing
- Third-party error handling MUST distinguish transient vs permanent failures
- Database schemas MUST be verified in deploy-check before build validation
- Serverless API routes MUST await all async calls before returning response
- Cron jobs MUST use batch fetching and concurrent Promise resolution
- Every GET/list query MUST enforce a hard take() clause (Prisma)
- No optimistic UI mutation without a backend persistence endpoint
- Telemetry MUST be implemented during feature development, not post-QA
- Unauthenticated endpoints calling paid APIs MUST specify rate limiting in the architecture spec
- SessionIds used across analytics + API + DB MUST be generated before any downstream operations
- AI calls on Vercel MUST use AbortController ≤ 9s and return JSON 504 on timeout
- Clipboard copy MUST have navigator.clipboard → execCommand fallback + inline error state
- All API route branches (success, timeout, error, rate-limit) MUST have PostHog events
These standards are enforced through:
- Code Review Agent: Checks for common violations
- Peer Review Agent: Adversarial architecture review
- QA Agent: Validates edge cases and error handling
- Deploy Check Agent: Verifies production readiness