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 script

Key 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.tsx

TypeScript & Type Safety

The project uses strict TypeScript with end-to-end type safety.

Type Safety Principles

  • No any types - use unknown or 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:

  1. State management (useState, useMemo)
  2. Data fetching (tRPC queries)
  3. Derived state (computed values)
  4. Loading states
  5. Empty states
  6. 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 deletedAt fields
  • 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 select to limit fields
  • Use include for 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 exist
  • UNAUTHORIZED - Not authenticated
  • FORBIDDEN - Insufficient permissions
  • BAD_REQUEST - Invalid input
  • CONFLICT - 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 select to 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