Development Guide
Technical documentation for developers
Architecture Overview
The Universal Annotation Tool is built with a modern, type-safe architecture.
Tech Stack
Frontend
- Next.js 15+ (App Router)
- TypeScript 5.3+
- shadcn/ui + Radix UI
- Tailwind CSS 4
- TanStack Query (server state)
- tRPC (type-safe RPC)
- Clerk (authentication)
Backend
- Next.js API Routes + tRPC
- PostgreSQL (via Supabase)
- Prisma ORM
- Redis 7 (session/state cache)
AI/ML Services
- FastAPI (Python 3.11+)
- REST API
- Docker containerization
Architecture Principles
- Type Safety First: End-to-end type safety from database to UI
- Feature-Based Organization: Group related code by feature, not by type
- Separation of Concerns: Clear boundaries between layers
- Progressive Enhancement: Start simple, add complexity only when needed
- Developer Experience: Prioritize DX with good tooling and clear patterns
Project Structure
The codebase follows a feature-based organization pattern.
Directory Layout
apps/web/src/
├── app/ # Next.js App Router pages
│ ├── [route]/
│ │ └── page.tsx
│ └── api/trpc/ # tRPC API handler
├── components/ # React components
│ ├── ui/ # Reusable UI primitives
│ ├── section/ # Layout components
│ └── [feature]/ # Feature-specific components
├── server/ # Server-side code
│ ├── routers/ # tRPC routers (by feature)
│ └── trpc.ts # tRPC setup
├── lib/ # Utilities and helpers
│ ├── db.ts # Prisma client
│ ├── trpc/ # tRPC client setup
│ └── auth/ # Auth utilities
└── prisma/
├── schema.prisma # Database schema
└── seed.ts # Seed scriptKey Principles
- Feature-based grouping: Components, routers, and utilities organized by feature domain
- Co-location: Related files stay together
- Clear separation: Server code, client code, and shared utilities are clearly separated
Code Organization
Consistent naming conventions and organization patterns make the codebase maintainable.
Naming Conventions
- Files: kebab-case (
user-management.tsx) - Components: PascalCase (
UserManagement) - Functions: camelCase (
getUserProfile) - Constants: UPPER_SNAKE_CASE (
MAX_FILE_SIZE) - Types/Interfaces: PascalCase (
UserProfile)
Component Organization
Components are organized by feature:
components/
├── ui/ # Reusable primitives
├── section/ # Layout components
└── collections/ # Feature: Collections
├── collections-list.tsx
├── create-collection-modal.tsx
└── collection-view.tsxTypeScript & Type Safety
The project uses strict TypeScript with end-to-end type safety.
Type Safety Principles
- No
anytypes - useunknownor proper types - Strict mode enabled in
tsconfig.json - Type inference when possible, explicit types for public APIs
tRPC Type Safety
tRPC provides end-to-end type safety:
- Server-side router types are automatically inferred on the client
- Input validation with Zod schemas
- Return types are fully typed
- No manual type definitions needed
Backend Development
Backend logic is implemented using tRPC routers with proper validation and error handling.
tRPC Router Pattern
Routers are organized by feature domain:
- Each router handles operations for a specific domain (e.g., collections, documents, annotations)
- Procedures are either queries (read) or mutations (write)
- Input validation with Zod schemas
- Proper error handling with TRPCError
Procedure Types
- publicProcedure: No authentication required
- protectedProcedure: Requires authentication
- adminProcedure: Requires admin role
- reviewerProcedure: Requires reviewer or admin role
Access Control
All resources are scoped to organizations:
- Verify user belongs to an organization
- Check resource belongs to user's organization
- Enforce owner-only access when needed
- Use role-based access control for admin features
Frontend Development
Frontend components follow consistent patterns for data fetching, state management, and UI.
Component Patterns
Components follow a consistent structure:
- State management (useState, useMemo)
- Data fetching (tRPC queries)
- Derived state (computed values)
- Loading states
- Empty states
- Render
Data Fetching
Use tRPC with TanStack Query:
- Automatic caching and refetching
- Optimistic updates for mutations
- Error handling built-in
- Loading states managed automatically
Form Patterns
Forms use controlled inputs with tRPC mutations:
- Controlled inputs with useState
- Validation with Zod (shared with backend)
- Error handling with toast notifications
- Success callbacks for cache invalidation
Database & Prisma
The database schema is managed with Prisma, providing type-safe database access.
Schema Design
Key patterns in the schema:
- Organization-scoped resources
- Soft deletes with
deletedAtfields - JSON fields for flexible data (
settings) - Proper indexes for performance
- Relations properly defined
Prisma Client
Use the singleton Prisma client:
- Prevents multiple connections in development
- Type-safe queries
- Automatic connection pooling
Query Optimization
Best practices:
- Use
selectto limit fields - Use
includefor relations - Add indexes for common query patterns
- Use pagination for large datasets
Authentication
Authentication is handled by Clerk, with automatic user sync to the database.
Clerk Integration
Clerk provides:
- User authentication (sign up, sign in)
- Session management
- User profile management
- Organization management (via Clerk Organizations)
User Sync
Users are automatically synced to the database:
- User created in DB on first protected procedure call
- Profile information synced from Clerk
- Role and organization assignment handled separately
Role-Based Access Control
Three roles with hierarchical permissions:
- Annotator: Basic annotation permissions
- Reviewer: Annotator + review permissions
- Admin: Full access
Error Handling
Consistent error handling patterns ensure good user experience and debugging.
Server-Side Errors
Use TRPCError with appropriate error codes:
NOT_FOUND- Resource doesn't existUNAUTHORIZED- Not authenticatedFORBIDDEN- Insufficient permissionsBAD_REQUEST- Invalid inputCONFLICT- Resource conflict
Client-Side Errors
Handle errors gracefully:
- Show user-friendly error messages
- Use toast notifications for feedback
- Display inline errors in forms
- Provide retry options when appropriate
Input Validation
Validate all inputs with Zod:
- Define schemas shared between frontend and backend
- Type inference from Zod schemas
- Clear error messages for validation failures
Best Practices
Follow these practices to maintain code quality and consistency.
General Principles
- Type safety: Always use TypeScript, avoid
any - Input validation: Validate all inputs with Zod
- Error handling: Handle errors gracefully with proper messages
- Loading states: Always show loading states for async operations
- Access control: Check permissions before allowing actions
- Code comments: Document complex logic and public APIs
- Consistent naming: Follow naming conventions consistently
- Small functions: Keep functions focused and small
- DRY principle: Don't repeat yourself, extract common logic
Performance
- Use
selectto limit database fields - Add indexes for common query patterns
- Use pagination for large datasets
- Optimize React renders with useMemo and useCallback
- Lazy load heavy components
Security
- Always validate input on the server
- Check permissions before operations
- Don't expose sensitive data in API responses
- Use parameterized queries (Prisma handles this)
- Sanitize user input to prevent XSS