Area Manager Frontend Architecture

Overview

The Area Manager feature is built using Next.js 15 App Router with Effect-TS ecosystem for type-safe functional programming. This document provides a high-level architectural overview of the frontend implementation.

The architecture follows Clean Architecture principles with clear separation between domain logic, application services, and presentation components. The feature is split into two main sub-pages (Managers and Rolling), each self-contained with its own domain models, services, and UI components.


Technology Stack

Core Framework

  • Next.js 15 - App Router with React Server Components
  • React 18 - UI library with server/client component split
  • TypeScript - Static typing throughout

Effect-TS Ecosystem

  • effect - Functional programming primitives and Schema validation
  • @effect-atom/atom-react - Reactive state management
  • @effect/platform - HTTP client abstractions
  • @effect/experimental - Reactivity layer for cache invalidation
  • AtomHttpApi - Type-safe API client generation

Supporting Libraries

  • TanStack Query v5 - Server state management (bridges to React)
  • React Hook Form - Form state management
  • Tailwind CSS + shadcn/ui - Styling and components
  • Lingui - Internationalization (i18n)

Project Structure

The feature follows a sub-page pattern where each major workflow has its own isolated domain and services:

admin-lite/src/app/admin/area-manager/
├── layout.tsx                        # Tab navigation wrapper
├── page.tsx                          # Root redirect
├── _domain/                          # Shared domain entities
├── _lib/                             # Shared services
├── _locales/                         # Shared translations
│
├── managers/                         # Managers Tab (CRUD operations)
│   ├── page.tsx                      # Main page
│   ├── _domain/
│   │   └── manager-entities.ts       # Schema.Class entities
│   ├── _lib/
│   │   ├── api-client.ts             # AtomHttpApi client
│   │   ├── manager-atoms.ts          # UI state atoms
│   │   ├── manager-repository.ts     # Data access
│   │   └── manager-queries.ts        # TanStack Query options
│   ├── _components/                  # Manager-specific UI
│   └── _locales/                     # Manager translations
│
└── rolling/                          # Rolling Tab (Transfer operations)
    ├── page.tsx                      # 3-step workflow page
    ├── _domain/
    │   ├── rolling-entities.ts       # Schema.Class entities
    │   ├── rolling-api.ts            # HttpApi contracts
    │   └── rolling-form-schema.ts    # Form validation schemas
    ├── _lib/
    │   ├── api-client.ts             # RollingClient (AtomHttpApi)
    │   ├── runtime.ts                # Effect runtime with layers
    │   ├── rolling-atoms.ts          # Reactive state atoms
    │   ├── rolling-repository.ts     # Data access layer
    │   ├── rolling-service.ts        # Business logic
    │   └── rolling-queries.ts        # TanStack Query bridge
    ├── _components/
    │   ├── site-selection/           # Step 1 components
    │   ├── transfer-config/          # Step 2 components
    │   ├── execution/                # Step 3 components
    │   └── progress-dialog/          # Real-time monitoring
    └── _locales/                     # Rolling translations

Key Principles:

  • Feature-first organization - Each sub-page is self-contained
  • Lazy extraction - Shared code moves to root _domain//_lib/ only when used by 2+ features
  • Co-location - Components live near the pages that use them

Architecture Layers

1. Domain Layer (_domain/)

Contains pure business entities and contracts with no external dependencies.

Entities are defined using effect/Schema.Class:

  • Runtime-validated data structures
  • Type inference from schema
  • Computed properties via getters
  • Immutable by design

Example entity with computed property:

export class RollingSite extends Schema.Class<RollingSite>('RollingSite')({
  siteID: Schema.String,
  name: Schema.String,
  team: Schema.optional(Schema.Array(Schema.String)),
}) {
  get assignedUserCount(): number {
    return this.team?.length || 0;
  }
}

API Contracts use @effect/platform HttpApi for type-safe endpoint definitions that generate both client and validation automatically.


2. Application Layer (_lib/)

Orchestrates business logic and data access between domain and presentation layers.

Components:

  1. API Client (api-client.ts)

    • Uses AtomHttpApi to generate type-safe API client from HttpApi contract
    • Integrates with effect-atom for reactive state
    • Handles authentication via layered HTTP client
  2. Runtime (runtime.ts)

    • Composes Effect layers (API client, reactivity, HTTP transport)
    • Provides dependency injection for the feature
    • Used by atoms for executing effects
  3. Atoms (*-atoms.ts)

    • UI state only (filters, selections, form state)
    • Derived atoms for computed values (counts, validations)
    • Write atoms for actions (add, remove, update)
    • URL state atoms for persistence (Atom.searchParam)
  4. Repository (*-repository.ts)

    • Data access layer extending BaseRepository
    • Wraps API calls with monitoring
    • Bridges to TanStack Query
  5. Service (*-service.ts)

    • Business logic extending BaseService
    • Validation and orchestration
    • Error handling with monitoring
  6. Queries (*-queries.ts)

    • TanStack Query queryOptions and mutationOptions
    • Bridges Effect services to React components
    • Manages server state caching and invalidation

3. Presentation Layer (_components/)

React components following Server Component by default pattern.

Component Types:

  • Server Components - Static pages, layouts, initial data fetching
  • Client Components - Interactive UI with 'use client' directive

State Access:

  • useAtomValue() - Read atom state
  • useAtomSet() - Write to atom (action)
  • useAtom() - Read + write
  • useQuery() / useMutation() - Server state

State Management Strategy

The architecture separates UI state from server state with clear boundaries:

UI State (effect-atom)

What it manages:

  • Filters and search inputs
  • Selected items (Map data structure for O(1) lookups)
  • Form state (before submission)
  • URL parameters (operation IDs, dialog state)
  • Derived/computed values (counts, validations)

Why effect-atom:

  • Reactive updates without re-renders
  • Derived atoms for memoized computations
  • URL state synchronization via Atom.searchParam
  • Type-safe with TypeScript inference
  • Integrates with Effect runtime

Atom patterns:

// State atom
export const selectedSitesAtom = Atom.make<Map<string, TransferSiteData>>(new Map());
 
// Derived atom (memoized)
export const selectedSitesCountAtom = Atom.make((get) => {
  return get(selectedSitesAtom).size;
});
 
// Action atom
export const addSiteAtom = Atom.writable(
  () => {},
  (ctx, site) => {
    const current = ctx.get(selectedSitesAtom);
    ctx.set(selectedSitesAtom, new Map([...current, [site.siteID, site]]));
  }
);
 
// URL state atom
export const operationIdAtom = Atom.searchParam('opsId', { schema: S.String });

Server State (TanStack Query)

What it manages:

  • API data fetching (sites, departments, operations)
  • Caching with stale-time configuration
  • Background refetching and polling
  • Optimistic updates
  • Request deduplication

Why TanStack Query:

  • Proven React ecosystem solution
  • Automatic cache invalidation
  • Built-in loading/error states
  • Polling support for real-time updates
  • DevTools for debugging

Query patterns:

export const rollingQueryOptions = {
  sites: (deptId: string, filters: Filters) => queryOptions({
    queryKey: ['rolling', 'sites', deptId, filters],
    queryFn: () => rollingService.getDepartmentSites(deptId, filters),
    staleTime: 30000, // 30s cache
  }),
 
  operation: (opsId: string | null) => queryOptions({
    queryKey: ['rolling', 'operation', opsId],
    queryFn: () => rollingService.getOperationStatus(opsId!),
    enabled: opsId !== null,
    refetchInterval: (query) => {
      // Poll every 3s if in progress, stop otherwise
      return query.state.data?.status === 'in_progress' ? 3000 : false;
    },
  }),
};

Data Flow

Flow 1: Filter → Query → Display

User Input (Search/Filter)
    ↓
UI State Atom Updated (setSearchAtom)
    ↓
Derived Atom Recomputes (apiFiltersAtom)
    ↓
TanStack Query Refetches (new filters in queryKey)
    ↓
Repository Calls API
    ↓
Data Returns & Cached
    ↓
Component Re-renders with New Data

Key: Atoms control the filters, TanStack Query handles the fetching.


Flow 2: User Action → State Update → Validation

User Selects Site
    ↓
Action Atom Executed (addSiteToTransferAtom)
    ↓
State Atom Updated (selectedSitesAtom)
    ↓
Derived Atoms Recompute (count, validation)
    ↓
UI Reactively Updates (buttons enable/disable)

Key: Immutable updates with new Map instances trigger reactive updates.


Flow 3: Execute → Optimistic UI → Real-time Updates

User Clicks Execute
    ↓
Mutation Starts (executeRolling)
    ↓
Optimistic Update (set operation ID in URL)
    ↓
Progress Dialog Opens (derived from URL param)
    ↓
API Call Executes
    ↓
Operation ID Returned
    ↓
Query Starts Polling (every 3s)
    ↓
Progress Updates Display
    ↓
Operation Completes
    ↓
Polling Stops (refetchInterval returns false)

Key: URL state persistence enables page reload without losing progress tracking.


Key Architectural Decisions

1. Schema.Class for Entities

Why: Runtime validation + type safety + computed properties in one construct.

Benefit: API responses are validated at runtime, preventing bad data from propagating. Getters provide clean derived data without manual computation.


2. AtomHttpApi for API Clients

Why: Type-safe API client generated from HttpApi contract, integrated with atoms.

Benefit: Single source of truth for API contracts. Client code generation ensures request/response types always match backend contract.


3. Effect Runtime for Dependency Injection

Why: Layer-based composition of services (HTTP client, auth, reactivity).

Benefit: Services are testable (layers can be mocked), and dependencies are explicit and type-safe.


4. Map for Selected Sites

Why: O(1) lookup/insert/delete instead of O(n) with arrays.

Benefit: Performance remains constant even with hundreds of selected sites.


5. URL State for Operation Tracking

Why: Atom.searchParam syncs operation ID to URL.

Benefit: Users can refresh the page during a rolling operation and progress monitoring continues. Shareable URLs for support.


6. Polling with Conditional Interval

Why: refetchInterval function stops polling when operation completes.

Benefit: No unnecessary API calls after operation finishes. Automatic cleanup.


Component Organization

Server Components (Default)

Used for:

  • Page shells (page.tsx)
  • Layouts (layout.tsx)
  • Static content containers

Benefits:

  • Zero JavaScript bundle
  • SEO-friendly
  • Fast initial load

Client Components ('use client')

Used for:

  • Forms with user input
  • Data tables with filters
  • Interactive dialogs
  • Real-time progress displays

Requirements:

  • Must use effect-atom hooks
  • Must use TanStack Query hooks
  • Have event handlers

Client component example:

'use client';
 
export function SitesTable() {
  const filters = useAtomValue(apiFiltersAtom);
  const { data } = useQuery(rollingQueryOptions.sites('dept-1', filters));
  return <Table data={data?.sites} />;
}

Integration Points

1. Authentication

Handled by AuthenticatedHttpClient layer provided to all API clients. Firebase auth token automatically attached to requests.


2. Monitoring

All repository and service methods wrapped with withMonitoring() for:

  • Operation timing
  • Error tracking (Sentry)
  • Business impact tagging
  • Context metadata

3. Internationalization

@lingui/react macros for translations:

  • <Trans> for component text
  • msg for dynamic strings
  • Compile-time extraction to _locales/

4. Error Handling

Pattern:

  • Repository: Catches HTTP errors, logs to Sentry
  • Service: Validates business rules, throws domain errors
  • Component: Displays user-friendly error messages via TanStack Query error state

Performance Characteristics

Optimization Strategies

  1. Derived Atoms - Memoized computations only run when dependencies change
  2. Map Data Structure - O(1) operations for large selections
  3. Query Stale Time - Prevents unnecessary refetches (30s default)
  4. Server Components - Reduces client JavaScript bundle
  5. Lazy Loading - Progress dialog code-split via lazy()
  6. Conditional Polling - Stops automatically when operation completes

Expected Performance

  • Initial Page Load: <1s (server component advantage)
  • Filter Change: <100ms (derived atom recompute + query cache hit)
  • Site Selection: <10ms (Map.set operation)
  • Execute Rolling: <200ms API round-trip, polling starts
  • Progress Update: 3s intervals, negligible overhead

Deployment

Built as Next.js standalone for Cloudflare Workers deployment:

  • Edge runtime for global low-latency
  • Environment variables via env.ts
  • Auto-deployment on push to master