> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Quality Attributes

> Maintainability, testability, and deployability in Tuturuuu's architecture

This document covers three critical quality attributes that are foundational to long-term system success: **Maintainability** (the ease of evolving and fixing the system), **Testability** (the ability to verify system correctness), and **Deployability** (the ease and safety of releasing changes).

<Note>
  These quality attributes are not added after the fact but are designed into the architecture from the beginning, ensuring the system remains healthy and productive as it grows.
</Note>

<Warning>
  **How to read this page.** Several patterns below (Hexagonal Architecture, ports/adapters, event sourcing, replay testing) are presented as **teaching models**, not as a description of the current codebase. Tuturuuu's apps are conventional **Next.js App Router** apps (plus a Rust backend) that share a single Supabase project — there are no `domain/`/`infrastructure/` hexagonal layers, no event store, and no per-app inter-service contracts package. Code samples that show those layers are **illustrative**; sections labeled **"In Tuturuuu today"** describe the real, verifiable structure. Treat the rest as design guidance for how we want isolated modules to behave.
</Warning>

<Info>
  **Active migration.** `apps/web` (Next.js, port `7803`) is being incrementally replaced by `apps/tanstack-web` (TanStack Start) plus `apps/backend` (Rust, port `7820`). The maintainability and deployability properties described here are being carried forward into that target architecture. See [TanStack + Rust Migration](/platform/architecture/tanstack-rust-migration) for the current state.
</Info>

<Info>
  To understand how different architectural patterns affect these quality attributes, see [Architectural Patterns Comparison](/platform/architecture/system-design/architectural-patterns-comparison).
</Info>

## Maintainability

Maintainability determines how easily the system can be modified to fix bugs, add features, or adapt to changing requirements. Poor maintainability leads to increasing development costs and slower delivery over time.

### 1. Clear Domain Boundaries Reduce Cognitive Load

#### Architectural Choice

The microservices architecture and modular frontend design establish clear boundaries between different business domains, each with well-defined responsibilities and interfaces.

#### Impact and Justification

In a monolithic, tightly-coupled system, understanding how to make a change requires comprehending vast swaths of interconnected code. A developer wanting to modify the billing logic might need to understand authentication, user management, and inventory systems simply because they're all tangled together.

**Clear domain boundaries** solve this by ensuring each service or module has a focused responsibility. A developer working on the `FinanceService` only needs to understand finance-related concepts and the clearly-defined interfaces for interacting with other services. This dramatically reduces cognitive load and enables faster, more confident changes.

**In Tuturuuu today:**

```text theme={null}
apps/
├── web/          // Main Next.js platform (port 7803) - workspace management
├── tanstack-web/ // TanStack Start successor to apps/web (migration in progress)
├── backend/      // Rust backend (port 7820)
├── finance/      // Finance surface
├── calendar/     // Calendar surface
├── tasks/        // Tasks surface
├── nova/         // AI challenges
├── rewise/       // AI chat
└── mobile/       // Flutter mobile app

// Each app owns a focused surface. The web-facing apps are conventional
// Next.js App Router apps; they share one Supabase project rather than
// each owning a private datastore. Cross-cutting product logic lives in
// shared packages (packages/internal-api, packages/types, packages/ui).
```

The boundary that reduces cognitive load here is the **app + feature folder**, not a separate per-service runtime with its own database. A developer working on finance can stay inside `apps/finance` (and the finance helpers in `packages/apis`/`packages/internal-api`) without learning the calendar surface.

**Module boundaries in frontend:**

```typescript theme={null}
// apps/web/src/app/[locale]/(dashboard)/[wsId]/
finance/
├── layout.tsx              // Finance-specific layout
├── transactions/           // Transaction management
├── reports/                // Financial reports
└── settings/               // Finance settings

// Finance module is self-contained
// Changes here don't affect calendar or tasks modules
```

#### Clarifying Additions

**Engineers focus on one area at a time without understanding the entire system.** New team members can become productive quickly by mastering a single domain rather than the entire codebase.

**This reduces onboarding complexity.** Clear boundaries make it obvious where to find code related to specific features, accelerating the learning curve for new developers.

**Maintenance tasks become easier to isolate and execute.** Bug fixes and feature additions are scoped to specific services or modules, reducing the risk of unintended side effects.

***

### 2. Technology Independence Prevents Legacy Lock-In

#### Architectural Choice

Isolating technology choices (databases, frameworks, external services) behind well-defined interfaces keeps core business logic decoupled from the tools that implement it.

#### Impact and Justification

Technology evolves rapidly. Frameworks fall out of favor, databases become outdated, and new tools emerge. Systems that tightly couple business logic to specific technologies become **legacy systems**—expensive and risky to modernize.

**Technology independence** protects the valuable business logic (which changes rarely) from technological churn (which happens frequently). When infrastructure details sit behind an interface, you can upgrade or replace technologies without rewriting business rules.

<Note>
  **Illustrative pattern.** The `WorkspaceRepository` / `SupabaseWorkspaceRepository` example below is a teaching model. Tuturuuu does **not** ship `packages/types/src/domain/workspace.ts` or `apps/web/src/infrastructure/repositories/*`; there is no repository-interface layer. In the real code, route handlers and shared helpers in `packages/internal-api` and `packages/apis` call the Supabase client directly (see "In Tuturuuu today" below). Read this as how to keep a swappable seam if you introduce one.
</Note>

```typescript theme={null}
// Illustrative: business logic expressed against an interface, not an implementation
export interface WorkspaceRepository {
  findById(id: string): Promise<Workspace | null>;
  save(workspace: Workspace): Promise<void>;
  delete(id: string): Promise<void>;
}

// Core business logic depends on the interface
export class WorkspaceService {
  constructor(private repo: WorkspaceRepository) {}

  async archiveWorkspace(id: string) {
    const workspace = await this.repo.findById(id);
    if (!workspace) throw new Error('Workspace not found');

    workspace.archive(); // Pure business logic
    await this.repo.save(workspace);
  }
}

// Illustrative infrastructure implementation (the swappable seam)
export class SupabaseWorkspaceRepository implements WorkspaceRepository {
  constructor(private client: SupabaseClient) {}

  async findById(id: string) {
    const { data } = await this.client
      .from('workspaces')
      .select('*')
      .eq('id', id)
      .single();

    return data ? this.toDomain(data) : null;
  }

  async save(workspace: Workspace) {
    await this.client
      .from('workspaces')
      .upsert(this.toDatabase(workspace));
  }

  async delete(id: string) {
    await this.client
      .from('workspaces')
      .delete()
      .eq('id', id);
  }
}

// Future: Can swap Supabase for Prisma, Drizzle, or any other ORM
// WITHOUT changing business logic
```

**Example technology evolution:**

```typescript theme={null}
// Today: Supabase
const repo = new SupabaseWorkspaceRepository(supabaseClient);

// Tomorrow: Drizzle ORM (business logic unchanged)
const repo = new DrizzleWorkspaceRepository(drizzleClient);

// Next year: Different database (business logic still unchanged)
const repo = new MongoWorkspaceRepository(mongoClient);

// Business logic stays the same:
const service = new WorkspaceService(repo);
await service.archiveWorkspace('ws-123');
```

**In Tuturuuu today:** technology independence comes from a few concrete seams rather than a full ports/adapters layer:

* Supabase access is centralized behind the `@tuturuuu/supabase/next/server` factories (`createClient()` and `createDynamicClient()` are async, `createAdminClient()` is sync). Swapping how a client is built happens in one place.
* Generated DB types are imported from `@tuturuuu/types/db`, so schema changes propagate through types instead of hand-written mappers.
* Shared product logic lives in `packages/internal-api` and `packages/apis`, so the web app and its TanStack successor can reuse the same helpers as the migration proceeds.

The biggest real-world example of this independence is the in-progress move of `apps/web` to `apps/tanstack-web` + a Rust `apps/backend`: shared packages let the data layer be re-pointed without rewriting every call site.

#### Clarifying Additions

**No single technical choice restricts future evolution.** The architecture accommodates technological progress without requiring expensive rewrites of core business logic.

**Components modernize incrementally as needed.** Instead of "big bang" technology migrations that put the entire system at risk, individual components can be upgraded independently.

**This protects the system from long-term stagnation.** The architecture remains flexible enough to adopt new technologies as they mature, preventing technical debt accumulation.

***

### 3. Hexagonal Architecture Minimizes Ripple Effects

#### Architectural Choice

The Hexagonal Architecture's strict separation between core domain logic and external concerns ensures that changes to infrastructure do not ripple into business rules.

#### Impact and Justification

In traditional layered architectures, changes to infrastructure (like upgrading a database library or switching message brokers) often require modifications to business logic because the layers are tightly coupled.

Hexagonal Architecture **inverts the dependency direction**. The core domain is completely isolated and defines interfaces (ports) for what it needs. Infrastructure adapters implement these interfaces but the core remains ignorant of implementation details.

This means infrastructure changes stay contained in the adapter layer. Upgrading from PostgreSQL 14 to PostgreSQL 16, changing from REST to GraphQL, or switching message brokers requires modifying only the relevant adapters—the domain core remains untouched.

<Note>
  **Illustrative pattern, not current code.** Tuturuuu does **not** implement a `TaskService` with injected `TaskRepository` / `EventBus` / `Logger` ports, and there is no `@tuturuuu/logging` or `@tuturuuu/observability` package. Task logic lives in conventional Next.js route handlers and shared helpers. The example below shows the *shape* of a hexagonal seam so you can recognize where one would help if a piece of logic becomes worth isolating. For background work, Tuturuuu uses **Trigger.dev v4 `task()`** definitions in `packages/trigger/src` — not a custom `EventBus`.
</Note>

```typescript theme={null}
// Illustrative domain core (unchanged when infrastructure changes)
export class TaskService {
  constructor(
    private taskRepo: TaskRepository,
    private eventBus: EventBus,
    private logger: Logger
  ) {}

  async createTask(data: CreateTaskData) {
    const task = Task.create(data);

    await this.taskRepo.save(task);
    await this.eventBus.publish(new TaskCreated(task));
    this.logger.info('Task created', { taskId: task.id });

    return task;
  }
}

// Infrastructure layer (changeable without affecting domain)
// Version 1: Supabase + Trigger.dev + Console logging
const service = new TaskService(
  new SupabaseTaskRepository(supabase),
  new TriggerEventBus(trigger),
  new ConsoleLogger()
);

// Version 2: Drizzle + NATS + Structured logging
// Domain core completely unchanged!
const service = new TaskService(
  new DrizzleTaskRepository(drizzle),
  new NatsEventBus(nats),
  new StructuredLogger(winston)
);
```

**Real-world example - Upgrading Next.js:**

```typescript theme={null}
// Business logic and components remain the same
// Only infrastructure adapters need updates

// Before Next.js 16
export function getWorkspaceData(id: string) {
  // Implementation using Next.js 15 patterns
}

// After Next.js 16
export function getWorkspaceData(id: string) {
  // Implementation using Next.js 16 patterns
  // Component code using this function unchanged
}
```

#### Clarifying Additions

**Core logic remains stable even during broad technical changes.** The most valuable part of the codebase (business rules) is shielded from the volatility of technology trends.

**Adaptation happens at edges rather than at the center.** When technology changes, only the outer layers (adapters) need modification, keeping changes localized and predictable.

**This protects business rules against unnecessary modification.** Domain experts can rely on business logic remaining stable, reducing the risk of introducing bugs when upgrading infrastructure.

***

### 4. Modular Frontend Prevents UI-Level Entanglement

#### Architectural Choice

The React Modular Monolith pattern organizes the frontend into self-contained modules with clear boundaries, preventing the "big ball of mud" problem common in large SPAs.

#### Impact and Justification

As frontend applications grow, they often become tangled webs of interdependent components, shared state, and implicit dependencies. Making a change in one area unexpectedly breaks seemingly unrelated features.

The **modular monolith** approach enforces architectural discipline by organizing code into feature-based modules with explicit boundaries. Each module (Finance, Calendar, Tasks, etc.) owns its components, state management, and business logic. Inter-module dependencies are explicit and minimized.

This structure makes maintenance dramatically easier because changes stay localized within module boundaries.

**In Tuturuuu today:** `apps/web` follows exactly this shape — feature folders under `app/[locale]/(dashboard)/[wsId]/` (finance, calendar, tasks, ...) each own their routes and colocated `_components/`. Client state uses **Jotai** (and TanStack Query for server data); the per-module `state.ts` paths below are a representative shape rather than fixed filenames.

```text theme={null}
apps/web/src/
├── app/[locale]/(dashboard)/[wsId]/
│   ├── finance/                    // Finance module
│   │   ├── layout.tsx
│   │   ├── transactions/
│   │   ├── reports/
│   │   └── _components/           // Finance-specific components
│   ├── calendar/                   // Calendar module
│   │   ├── layout.tsx
│   │   ├── events/
│   │   └── _components/           // Calendar-specific components
│   └── tasks/                      // Tasks module
│       ├── layout.tsx
│       ├── boards/
│       └── _components/           // Task-specific components
├── components/
│   ├── finance/                   // Shared finance components
│   ├── calendar/                  // Shared calendar components
│   ├── tasks/                     // Shared task components
│   └── common/                    // Truly shared components
```

**Module independence:**

```typescript theme={null}
// Finance module - self-contained state management
// apps/web/src/app/[locale]/(dashboard)/[wsId]/finance/state.ts
import { atom } from 'jotai';

export const financeTransactionsAtom = atom<Transaction[]>([]);
export const financeFiltersAtom = atom<Filters>({});

// Calendar module - separate state management
// apps/web/src/app/[locale]/(dashboard)/[wsId]/calendar/state.ts
import { atom } from 'jotai';

export const calendarEventsAtom = atom<Event[]>([]);
export const calendarViewAtom = atom<'month' | 'week' | 'day'>('month');

// Modules don't share state - clear boundaries
```

#### Clarifying Additions

**UI concerns grow independently instead of interfering with each other.** Finance features can evolve without touching calendar code, and vice versa.

**Teams can refactor modules safely.** Because module boundaries are clear, teams can confidently restructure their module knowing they won't break other features.

**Visual and functional changes stay well-scoped.** UI redesigns or feature enhancements affect only the relevant module, reducing regression testing burden.

***

### 5. Operational Independence per Service

#### Architectural Choice

Each microservice can be deployed, scaled, monitored, and maintained independently without coordinating with other services.

#### Impact and Justification

In a monolithic architecture, operational tasks affect the entire system. Deploying a minor bug fix to the reporting module requires rebuilding and redeploying the entire application. Scaling requires scaling everything, even if only one feature is under load.

**Operational independence** means each service is a separate deployable unit with its own lifecycle. Teams can:

* Deploy updates to their service multiple times per day
* Scale their service based on its specific load patterns
* Monitor their service's health independently
* Apply operational changes (like upgrading Node.js) on their own schedule

This independence accelerates development velocity and reduces the coordination overhead that plagues monolithic systems.

**In Tuturuuu today:** there are no `deploy:web` / `deploy:finance` / `deploy:calendar` package scripts. Independence is achieved through **per-app Vercel projects** wired up by dedicated GitHub Actions workflows. Each app has its own preview workflow under `.github/workflows/` (for example `vercel-preview-finance.yaml`, `vercel-preview-calendar.yaml`, `vercel-preview-drive.yaml`), and Turborepo only rebuilds the apps affected by a change:

```bash theme={null}
# Build a single workspace and its dependency graph (Turborepo filters by app)
bun turbo run build --filter=@tuturuuu/web

# Run an app locally (each app owns its own dev script)
cd apps/finance && bun dev
```

```yaml theme={null}
# Real per-app preview workflow — .github/workflows/vercel-preview-finance.yaml
name: Vercel Finance Preview Deployment
# Builds only the shared deps the finance app needs, then deploys it to Vercel:
#   bun turbo:local run build --filter=@tuturuuu/types --filter=@tuturuuu/supabase --filter=@tuturuuu/internal-api
```

Because each app is its own Vercel project, finance can deploy without rebuilding or redeploying the web or calendar apps.

**Monitoring:** Tuturuuu does not ship a custom per-service metrics SDK. Runtime/server logs in API, cron, and infrastructure code use the native console method matching severity, while retained request, cron, deployment, and usage data still flows through the internal log-drain storage where those wrappers are wired. Vercel provides per-project deployment and runtime insight. Treat the `monitorErrorRate(...)` snippet style as conceptual.

#### Clarifying Additions

**Services update without impacting other services.** A finance service deployment doesn't require coordination with the calendar team or risk affecting calendar functionality.

**Maintenance tasks stay focused and manageable.** Security patches, dependency updates, and refactoring efforts are scoped to individual services.

**This encourages timely updates and better long-term health.** Teams aren't hesitant to deploy updates because the blast radius is limited to their service.

***

## Testability

Testability determines how easily we can write and maintain automated tests that verify system correctness. High testability leads to better quality, faster development, and greater confidence in changes.

### 1. Pure Logic Enables Fast Unit Testing

#### Architectural Choice

Keeping pure logic free of infrastructure dependencies enables fast unit tests that don't require databases, network calls, or other external systems. (A full Hexagonal layer is one way to enforce this; Tuturuuu does not implement that layering, but the *principle* — isolate decision logic from I/O — still applies inside conventional modules.)

#### Impact and Justification

In traditional architectures, business logic is often tightly coupled to infrastructure (database access, API calls, file I/O). This makes testing painful because every test requires:

* Setting up test databases
* Mocking network requests
* Managing test data cleanup
* Dealing with test environment flakiness

These tests are slow (seconds per test instead of milliseconds), fragile (break when infrastructure changes), and difficult to write (require complex setup).

Isolating pure logic solves this by making the decision code free of infrastructure dependencies. Tests instantiate the object and verify rules with simple in-memory inputs. These tests run in milliseconds and never fail due to infrastructure issues.

<Note>
  **Illustrative pattern.** The `Workspace` class below does not exist at `packages/types/src/domain/workspace.ts` (there is no `domain/` directory). It models how a pure, framework-free unit would look. Real tests in the repo run on **Vitest** (`vitest run`) and live next to the code they cover as `*.test.ts` — for example the route handler tests under `packages/apis/src/finance/**`.
</Note>

```typescript theme={null}
// Illustrative pure logic — no I/O, fully testable
export class Workspace {
  private constructor(
    public readonly id: string,
    public name: string,
    public status: 'active' | 'archived',
    public memberLimit: number
  ) {}

  static create(data: CreateWorkspaceData): Workspace {
    if (data.name.length < 3) {
      throw new Error('Workspace name must be at least 3 characters');
    }

    return new Workspace(
      uuid(),
      data.name,
      'active',
      data.memberLimit || 10
    );
  }

  archive() {
    if (this.status === 'archived') {
      throw new Error('Workspace already archived');
    }
    this.status = 'archived';
  }

  addMember() {
    // Business rule: enforce member limit
    if (this.getMemberCount() >= this.memberLimit) {
      throw new Error('Member limit reached');
    }
    // ... add member logic
  }
}

// Test - no infrastructure required (Vitest, colocated *.test.ts)
describe('Workspace', () => {
  describe('create', () => {
    it('creates workspace with valid name', () => {
      const workspace = Workspace.create({
        name: 'My Workspace',
        memberLimit: 10
      });

      expect(workspace.name).toBe('My Workspace');
      expect(workspace.status).toBe('active');
      expect(workspace.memberLimit).toBe(10);
    });

    it('rejects workspace with short name', () => {
      expect(() => {
        Workspace.create({ name: 'ab', memberLimit: 10 });
      }).toThrow('Workspace name must be at least 3 characters');
    });
  });

  describe('archive', () => {
    it('archives active workspace', () => {
      const workspace = Workspace.create({ name: 'Test', memberLimit: 10 });

      workspace.archive();

      expect(workspace.status).toBe('archived');
    });

    it('rejects archiving already archived workspace', () => {
      const workspace = Workspace.create({ name: 'Test', memberLimit: 10 });
      workspace.archive();

      expect(() => workspace.archive()).toThrow('Workspace already archived');
    });
  });
});

// Tests run in <1ms each, no database required
```

#### Clarifying Additions

**Domain logic is easy to verify because it avoids external dependencies.** Tests focus purely on business rules without the complexity of infrastructure setup.

**Tests remain fast and reliable.** Unit tests execute in milliseconds, enabling tight feedback loops and encouraging developers to run tests frequently.

**This leads to strong coverage of critical business rules.** Because tests are easy to write, developers write more of them, leading to better quality and fewer bugs.

***

### 2. Adapter Design Enables Test Doubles

#### Architectural Choice

All external dependencies are accessed through interfaces (ports), allowing test implementations (test doubles) to be easily substituted during testing.

#### Impact and Justification

Real external systems (databases, APIs, message brokers) are problematic for testing:

* **Slow**: Network calls and database queries add latency
* **Flaky**: External systems can be temporarily unavailable
* **Stateful**: Tests can interfere with each other through shared state
* **Complex**: Require extensive setup and teardown

**Test doubles** (in-memory implementations of interfaces) solve all these problems. They're fast, reliable, isolated, and simple. The pattern works wherever a dependency is reachable behind an interface or an injected client.

<Note>
  **Illustrative pattern.** `TaskRepository` / `InMemoryTaskRepository` is a model, not repo code. In practice, Tuturuuu tests usually mock the Supabase client (or use Vitest mocks) at the boundary of a route handler rather than swapping a hand-written repository port. The shape below shows the underlying idea: keep the seam mockable.
</Note>

```typescript theme={null}
// Illustrative interface seam
export interface TaskRepository {
  findById(id: string): Promise<Task | null>;
  save(task: Task): Promise<void>;
  findByWorkspace(workspaceId: string): Promise<Task[]>;
}

// Production implementation (Supabase)
export class SupabaseTaskRepository implements TaskRepository {
  // ... real database implementation
}

// Test implementation (in-memory)
export class InMemoryTaskRepository implements TaskRepository {
  private tasks = new Map<string, Task>();

  async findById(id: string): Promise<Task | null> {
    return this.tasks.get(id) || null;
  }

  async save(task: Task): Promise<void> {
    this.tasks.set(task.id, task);
  }

  async findByWorkspace(workspaceId: string): Promise<Task[]> {
    return Array.from(this.tasks.values())
      .filter(task => task.workspaceId === workspaceId);
  }

  // Test helpers
  clear() {
    this.tasks.clear();
  }

  count() {
    return this.tasks.size;
  }
}

// Test using in-memory repository
describe('TaskService', () => {
  let taskRepo: InMemoryTaskRepository;
  let service: TaskService;

  beforeEach(() => {
    taskRepo = new InMemoryTaskRepository();
    service = new TaskService(taskRepo);
  });

  it('creates task', async () => {
    const task = await service.createTask({
      title: 'Test task',
      workspaceId: 'ws-123'
    });

    expect(task.title).toBe('Test task');
    expect(taskRepo.count()).toBe(1);

    // Verify task was saved
    const saved = await taskRepo.findById(task.id);
    expect(saved).toBeDefined();
  });

  it('finds tasks by workspace', async () => {
    await service.createTask({ title: 'Task 1', workspaceId: 'ws-1' });
    await service.createTask({ title: 'Task 2', workspaceId: 'ws-1' });
    await service.createTask({ title: 'Task 3', workspaceId: 'ws-2' });

    const tasks = await taskRepo.findByWorkspace('ws-1');

    expect(tasks).toHaveLength(2);
  });
});
```

#### Clarifying Additions

**External interactions can be simulated cleanly.** Test doubles provide predictable, controllable implementations of external systems.

**Testing becomes predictable and thorough.** Tests aren't affected by external system availability or performance, leading to consistent, reliable test suites.

**Complex conditions are easier to replicate.** Error scenarios (network failures, database constraints) can be easily simulated with test doubles that would be difficult to reproduce with real systems.

***

### 3. Contract Testing for Microservices

#### Architectural Choice

Service boundaries are verified through contract tests that ensure APIs and event schemas remain compatible as services evolve independently.

#### Impact and Justification

In a microservices architecture, services evolve independently. A breaking change to a service's API can silently break consumers without the producer knowing until runtime. Traditional integration tests that spin up all services are slow, complex, and brittle.

**Contract testing** solves this by verifying that:

1. **Producer** honors the contract (API/event schema) that consumers expect
2. **Consumer** can handle the responses/events that producers emit

This catches breaking changes early (at build time) without requiring full integration test environments.

<Note>
  **Illustrative pattern.** There is no `packages/apis/src/contracts/workspace.contract.ts`, and Tuturuuu does not run formal producer/consumer contract tests today. `packages/apis` (`@tuturuuu/apis`) holds **REST route handlers** (e.g. `src/finance/debts/route.ts`) with colocated `*.test.ts` files — not contract schemas. The closest real practice is validating request/response payloads with **Zod** inside route handlers and asserting on them in tests. The schema/contract code below is a conceptual model for how an explicit contract would look if introduced.
</Note>

```typescript theme={null}
// Illustrative API contract definition
import { z } from 'zod';

export const WorkspaceContract = {
  getWorkspace: {
    input: z.object({
      id: z.guid()
    }),
    output: z.object({
      id: z.guid(),
      name: z.string(),
      status: z.enum(['active', 'archived']),
      memberCount: z.number(),
      createdAt: z.string().datetime()
    })
  },

  createWorkspace: {
    input: z.object({
      name: z.string().min(3),
      memberLimit: z.number().optional()
    }),
    output: z.object({
      id: z.guid(),
      name: z.string(),
      status: z.literal('active'),
      memberLimit: z.number()
    })
  }
};

// Producer test - verify service honors contract
describe('WorkspaceAPI - Producer Contract', () => {
  it('getWorkspace returns valid response', async () => {
    const workspace = await createTestWorkspace();

    const response = await api.getWorkspace({ id: workspace.id });

    // Verify response matches contract
    expect(() => {
      WorkspaceContract.getWorkspace.output.parse(response);
    }).not.toThrow();
  });

  it('createWorkspace accepts valid input and returns valid output', async () => {
    const input = { name: 'Test Workspace', memberLimit: 20 };

    // Verify input is valid
    expect(() => {
      WorkspaceContract.createWorkspace.input.parse(input);
    }).not.toThrow();

    const response = await api.createWorkspace(input);

    // Verify output matches contract
    expect(() => {
      WorkspaceContract.createWorkspace.output.parse(response);
    }).not.toThrow();
  });
});

// Consumer test - verify consumer can handle producer responses
describe('WorkspaceAPI - Consumer Contract', () => {
  it('handles workspace response', () => {
    // Mock response that matches contract
    const mockResponse = {
      id: '123e4567-e89b-12d3-a456-426614174000',
      name: 'My Workspace',
      status: 'active' as const,
      memberCount: 5,
      createdAt: '2024-11-17T00:00:00Z'
    };

    // Consumer code should handle this
    expect(() => {
      const workspace = parseWorkspaceResponse(mockResponse);
      expect(workspace.name).toBe('My Workspace');
    }).not.toThrow();
  });
});
```

**Event contract testing:**

```typescript theme={null}
// Event schema contract
export const WorkspaceEvents = {
  'workspace.created': z.object({
    workspaceId: z.guid(),
    name: z.string(),
    ownerId: z.guid(),
    createdAt: z.string().datetime()
  }),

  'workspace.member.added': z.object({
    workspaceId: z.guid(),
    userId: z.guid(),
    role: z.enum(['owner', 'admin', 'member']),
    addedAt: z.string().datetime()
  })
};

// Producer test
it('emits valid workspace.created event', async () => {
  const event = await captureEvent('workspace.created', () => {
    return createWorkspace({ name: 'Test' });
  });

  // Verify event matches schema
  expect(() => {
    WorkspaceEvents['workspace.created'].parse(event);
  }).not.toThrow();
});

// Consumer test
it('handles workspace.created event', async () => {
  const mockEvent = {
    workspaceId: '123e4567-e89b-12d3-a456-426614174000',
    name: 'Test Workspace',
    ownerId: '234e5678-e89b-12d3-a456-426614174000',
    createdAt: '2024-11-17T00:00:00Z'
  };

  // Consumer should handle valid events
  expect(async () => {
    await handleWorkspaceCreated(mockEvent);
  }).not.toThrow();
});
```

#### Clarifying Additions

**Service boundaries remain stable through contract checks.** Breaking changes are caught by failing contract tests before code reaches production.

**Teams catch breaking changes early.** Instead of discovering API incompatibilities in staging or production, contract tests fail during CI/CD.

**Collaboration between services stays reliable.** Contract tests act as executable documentation of service interfaces, ensuring compatibility as teams work independently.

***

### 4. Event-Driven Systems Support Replay Testing

#### Architectural Choice

The event-driven architecture preserves event history, enabling powerful testing techniques like event replay and scenario recreation.

#### Impact and Justification

Traditional request-response systems are ephemeral. Once a request completes, reproducing the exact conditions that led to a bug is difficult or impossible. Debugging production issues often involves guesswork.

**Event replay** provides a time machine for testing. Because all events are preserved, we can:

1. **Capture** event sequences from production
2. **Replay** them in test environments to reproduce bugs
3. **Verify** that fixes resolve the issue
4. **Test** edge cases by constructing specific event sequences

This makes debugging significantly more reliable and enables regression testing for complex, multi-step workflows.

<Warning>
  **Aspirational pattern — not implemented.** Tuturuuu has **no event store and no event-sourcing layer**, so there is no `eventStore.query(...)` or production event replay. Background work runs as **Trigger.dev v4 `task()`** definitions (`packages/trigger/src`), which are individually testable but are not a replayable event log. The code below illustrates the *concept* of replay testing; do not treat it as an available API.
</Warning>

```typescript theme={null}
// Aspirational: capture events from an event store (not present in Tuturuuu)
export async function captureEventSequence(
  workspaceId: string,
  startTime: Date,
  endTime: Date
): Promise<DomainEvent[]> {
  return await eventStore.query({
    workspaceId,
    from: startTime,
    to: endTime,
    orderBy: 'timestamp'
  });
}

// Replay events in test environment
export async function replayEvents(events: DomainEvent[]) {
  // Reset test environment
  await resetTestDatabase();

  // Replay each event in order
  for (const event of events) {
    await eventBus.publish(event);
    await waitForEventProcessing();
  }
}

// Test by replaying production scenario
describe('Bug: Workspace members not receiving invitations', () => {
  it('reproduces production bug', async () => {
    // Capture events from production incident
    const events = await captureEventSequence(
      'ws-problematic',
      new Date('2024-11-15T14:00:00Z'),
      new Date('2024-11-15T14:05:00Z')
    );

    // Replay in test environment
    await replayEvents(events);

    // Verify bug is reproduced
    const invitations = await getInvitations('ws-problematic');
    expect(invitations).toHaveLength(0); // Bug: no invitations sent
  });

  it('verifies fix resolves the issue', async () => {
    // Apply the fix (code changes)

    // Replay same event sequence
    const events = await captureEventSequence(
      'ws-problematic',
      new Date('2024-11-15T14:00:00Z'),
      new Date('2024-11-15T14:05:00Z')
    );

    await replayEvents(events);

    // Verify fix worked
    const invitations = await getInvitations('ws-problematic');
    expect(invitations).toHaveLength(3); // Fix: invitations sent correctly
  });
});

// Test edge cases by constructing event sequences
describe('Workspace state transitions', () => {
  it('handles rapid member additions and removals', async () => {
    const events = [
      createEvent('workspace.created', { id: 'ws-1' }),
      createEvent('member.added', { userId: 'user-1' }),
      createEvent('member.added', { userId: 'user-2' }),
      createEvent('member.removed', { userId: 'user-1' }),
      createEvent('member.added', { userId: 'user-3' }),
      createEvent('member.removed', { userId: 'user-2' }),
    ];

    await replayEvents(events);

    const workspace = await getWorkspace('ws-1');
    expect(workspace.members).toHaveLength(1);
    expect(workspace.members[0].userId).toBe('user-3');
  });
});
```

#### Clarifying Additions

**Capturing event sequences makes reproducing issues easier.** Production bugs can be reliably recreated in test environments by replaying the exact sequence of events that led to the failure.

**Historical flows can be studied and validated.** Teams can analyze how the system responded to past events and verify that fixes prevent recurrence.

**This strengthens debugging across distributed processes.** Complex, multi-service workflows can be tested end-to-end by replaying events, making it easier to verify system behavior.

***

### 5. Independent CI Pipelines

#### Architectural Choice

Each microservice has its own independent CI/CD pipeline that runs tests, builds, and deploys the service without coordinating with other services.

#### Impact and Justification

In a monolithic architecture, all tests run in a single, long CI pipeline. A failure in any part of the system blocks the entire deployment, and slow tests create bottlenecks.

**Independent CI pipelines** for each service provide:

* **Parallelism**: All services test simultaneously, dramatically reducing feedback time
* **Isolation**: Failures in one service don't block others from deploying
* **Focus**: Teams see only their service's test results, reducing noise
* **Speed**: Smaller test suites run faster than monolithic suites

**In Tuturuuu today:** there are no `web-ci.yml` / `finance-ci.yml` per-app pipeline files. Instead, shared workflows test the whole graph with Turborepo's affected-package filtering, and **per-app Vercel preview workflows** handle build/deploy. Real workflows under `.github/workflows/` include:

* `turbo-unit-tests.yaml` — runs unit tests across affected workspaces
* `type-check.yaml` — type checking
* `biome-check.yaml` — lint/format (Tuturuuu uses **Biome**, not ESLint/Prettier)
* `ci-check.yml` — validates CI configuration
* `vercel-preview-finance.yaml`, `vercel-preview-calendar.yaml`, `vercel-preview-drive.yaml`, ... — one preview-deploy workflow per app
* `rust-backend.yml` — builds/tests `apps/backend`

The independence comes from Turborepo only running tasks for changed packages plus separate Vercel projects per app, rather than from one CI file per service.

```bash theme={null}
# How affected work is scoped locally and in CI
bun turbo run test --filter=@tuturuuu/web
bun turbo run build --filter=@tuturuuu/finance
```

**Turborepo task configuration.** The repo runs **Turborepo 2.x**, which uses the `tasks` key (the older `pipeline` key was removed in v2):

```json theme={null}
// turbo.json (real key is "tasks", not "pipeline")
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "test": {
      "cache": true
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"],
      "cache": true
    }
  }
}
```

#### Clarifying Additions

**Tests run in parallel without interfering with each other.** Multiple teams can have their tests running simultaneously, reducing wait times.

**Failures stay localized to the affected service.** A failing test in the finance service doesn't prevent the web app from deploying.

**Delivery becomes smoother and less risky.** Smaller, focused deployments are easier to verify and roll back if needed.

***

## Deployability

Deployability determines how easily and safely we can release changes to production. Good deployability enables frequent, low-risk deployments.

### 1. Independent Deployment Pipelines

#### Architectural Choice

Each microservice is independently deployable with its own release cycle, allowing teams to ship updates without coordinating across the organization.

#### Impact and Justification

Monolithic architectures force **all-or-nothing deployments**. Every change, no matter how small or isolated, requires rebuilding and redeploying the entire application. This creates:

* **Deployment bottlenecks**: Teams must coordinate release windows
* **High-risk releases**: Large deployments contain many changes, increasing failure probability
* **Slow feedback**: Small fixes take days to reach production
* **Deployment fear**: Teams avoid deploying due to coordination overhead

**Independent deployment** eliminates these problems. Each service ships on its own schedule:

* **Fast iteration**: Deploy bug fixes minutes after they're written
* **Low risk**: Small, focused deployments are easier to verify and roll back
* **Team autonomy**: No cross-team coordination required for deployments
* **Continuous delivery**: Deploy multiple times per day without fear

**In Tuturuuu today:** there is no `bun deploy:web` / `bun deploy:finance` / `bun deploy:calendar` command. Each app is a **separate Vercel project**, driven by its own GitHub Actions workflow, so deploys are already decoupled per app:

```text theme={null}
.github/workflows/
├── vercel-preview-finance.yaml    // Finance app preview/deploy
├── vercel-preview-calendar.yaml   // Calendar app preview/deploy
├── vercel-preview-drive.yaml      // Drive app preview/deploy
├── vercel-preview-apps.yaml       // Shared preview orchestration
└── rust-backend.yml               // apps/backend (Rust) build + deploy
```

Each workflow builds only the shared dependencies that app needs (via Turborepo `--filter`) and ships that one Vercel project. Deploying finance does not rebuild or redeploy web or calendar.

<Note>
  The Rust `apps/backend` (port `7820`) deploys on its own track (`rust-backend.yml`), independent of the Vercel front-ends — another axis of deployment independence introduced by the migration.
</Note>

#### Clarifying Additions

**Teams ship updates without affecting others.** Finance deployments don't trigger web redeployments or risk breaking calendar functionality.

**Releases become smaller and less risky.** Instead of deploying 50 changes from 10 teams, each team deploys 5 changes independently.

**This improves overall delivery speed.** Removing coordination overhead and deployment fear leads to more frequent, confident releases.

***

### 2. Controlled Rollout Strategies Through Architectural Boundaries

#### Architectural Choice

Service boundaries enable granular deployment strategies like canary releases, blue-green deployments, and feature flags, allowing changes to be introduced gradually.

#### Impact and Justification

Even with small deployments, risk remains. A bug can slip through testing and impact production users. **Controlled rollout strategies** mitigate this risk by:

* **Gradual exposure**: New versions serve only a small percentage of traffic initially
* **Monitoring**: Observe error rates and performance before full rollout
* **Quick rollback**: Instantly revert to previous version if issues detected
* **Confidence**: Deploy with confidence knowing failures impact minimal users

Service boundaries make these strategies practical because each service can have independent routing and versioning.

<Note>
  **Mixed: real platform capability + illustrative code.** Per-app Vercel projects give Tuturuuu **atomic, instantly-revertible deployments** and preview URLs per change — that part is real. However, Tuturuuu does **not** ship the `featureFlags.get/enable(...)` service or the `switchTraffic('blue' -> 'green')` / `supabase.rpc('run_migration')` orchestration shown below; those are conceptual illustrations of canary, feature-flag, and blue-green strategies, not repo APIs. (Tuturuuu's Supabase migrations are plain SQL applied via the migration workflow, not an RPC call.)
</Note>

```typescript theme={null}
// Conceptual: canary deployment (Vercel handles atomic deploy + instant rollback)
// New version serves 10% of traffic initially

// vercel.json
{
  "name": "tuturuuu-finance",

  // Vercel automatically does canary deployments
  // 1. Deploy new version
  // 2. Route 10% traffic to new version
  // 3. Monitor error rates
  // 4. Gradually increase to 100% if healthy
  // 5. Automatic rollback if error rates spike
}

// Feature flags for gradual feature rollout
export async function getFinanceFeatures(workspaceId: string) {
  const flags = await featureFlags.get(workspaceId);

  return {
    advancedReports: flags.has('finance.advanced-reports'),
    aiForecasting: flags.has('finance.ai-forecasting'),
    multiCurrency: flags.has('finance.multi-currency'),
  };
}

// Enable feature for specific workspaces first
await featureFlags.enable('finance.ai-forecasting', {
  workspaces: ['ws-beta-1', 'ws-beta-2'] // Beta test
});

// Monitor performance and errors
const metrics = await monitorFeature('finance.ai-forecasting', '24h');

if (metrics.errorRate < 0.01 && metrics.userSatisfaction > 0.8) {
  // Gradually roll out to more users
  await featureFlags.enable('finance.ai-forecasting', {
    percentage: 50 // 50% of all workspaces
  });
}

// Full rollout after validation
await featureFlags.enable('finance.ai-forecasting', {
  percentage: 100 // Everyone
});
```

**Blue-green deployment (conceptual snippet).** Tuturuuu's self-hosted Docker deploy does use a real blue/green release watcher (snapshot + cutover) for the web app, but it is driven by deployment scripts, not the `switchTraffic(...)` / `supabase.rpc(...)` calls shown here. Read this as the idea, not the API:

```typescript theme={null}
// Run migrations on "green" environment
// Switch traffic after validation

// 1. Green environment: Apply migration
await supabase.rpc('run_migration', {
  migration: '20241117_add_ai_forecasting'
});

// 2. Verify green environment health
const healthy = await verifyGreenEnvironment();

if (healthy) {
  // 3. Switch traffic from blue to green
  await switchTraffic('blue' -> 'green');

  // 4. Monitor for issues
  setTimeout(() => {
    if (metricsLookGood()) {
      // 5. Destroy blue environment
      destroyBlueEnvironment();
    } else {
      // Rollback: Switch back to blue
      switchTraffic('green' -> 'blue');
    }
  }, 3600000); // Monitor for 1 hour
}
```

#### Clarifying Additions

**New versions introduce changes gradually instead of all at once.** Risk is minimized by limiting exposure to a small percentage of users or specific cohorts.

**Issues surface early in controlled conditions.** Problems are detected when only 10% of users are affected, allowing quick rollback before widespread impact.

**This improves deployment safety.** Teams deploy confidently knowing that issues will be caught early and can be mitigated quickly.

***

### 3. Architecture-Supported Environment Consistency

#### Architectural Choice

Infrastructure as Code (IaC) and containerization ensure that development, staging, and production environments are consistent and reproducible.

#### Impact and Justification

The classic "works on my machine" problem stems from environment inconsistencies. Code behaves differently in production than in development due to different:

* Dependencies and versions
* Environment variables and configuration
* Infrastructure and scaling behavior
* Operating systems and system libraries

**Architecture-supported environment consistency** eliminates this by:

* **Defining environments as code** that can be version-controlled
* **Using containers** to ensure identical runtime across environments
* **Automating setup** to prevent manual configuration drift
* **Validating consistency** through automated checks

**In Tuturuuu - Environment consistency:**

```typescript theme={null}
// .env.example - template for all environments
// Developers, staging, and production use same variables

NEXT_PUBLIC_SUPABASE_URL=
SUPABASE_SECRET_KEY=
TRIGGER_API_KEY=
DATABASE_URL=

// Environment-specific .env files
// .env.local (development)
// .env.staging (staging)
// .env.production (production)

// All have same structure, different values
```

**Vercel environment configuration:**

```json theme={null}
// vercel.json ensures consistency
{
  "env": {
    // Required in all environments
    "NEXT_PUBLIC_SUPABASE_URL": "@supabase-url",
    "SUPABASE_SECRET_KEY": "@supabase-service-key"
  },

  "build": {
    "env": {
      // Build-time variables. The platform version is owned by Release Please
      // and read from TUTURUUU_PLATFORM_VERSION in
      // packages/utils/src/platform-release.ts — do not hard-code a literal here.
      "NEXT_PUBLIC_APP_VERSION": "$TUTURUUU_PLATFORM_VERSION"
    }
  }
}
```

**Pinned toolchain ensures runtime consistency.** The repo pins its toolchain so every environment resolves identical versions: `packageManager` is `bun@1.3.14`, and Node `engines` require `>=22`. A container image would mirror those pins:

```dockerfile theme={null}
# Illustrative Dockerfile — keep versions aligned with package.json pins
FROM oven/bun:1.3.14 as base

WORKDIR /app

# Install dependencies (frozen lockfile => same versions everywhere)
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile

COPY . .
RUN bun run build

# Production image (identical across environments)
FROM oven/bun:1.3.14-slim
WORKDIR /app
COPY --from=base /app/.next ./.next
COPY --from=base /app/node_modules ./node_modules
COPY --from=base /app/package.json ./

CMD ["bun", "run", "start"]
```

**Supabase local development matches production.** Migrations are written once and applied identically; type generation keeps types in sync:

```bash theme={null}
# Start local Supabase (mirrors the production schema)
bun sb:start

# Author and apply migrations locally
bun sb:new      # scaffold a new migration
bun sb:up       # apply migrations to the local database

# Regenerate DB types after schema changes
bun sb:typegen
```

<Warning>
  Production migrations are applied by a maintainer, not from agent/CI prose. Do **not** run `bun sb:push` / `bun sb:linkpush` from this workflow — prepare the migration and let the owner apply it.
</Warning>

#### Clarifying Additions

**Environments remain aligned through structured definitions.** Configuration as code prevents drift between development, staging, and production.

**Deployment steps become predictable across stages.** The same deployment process works identically in all environments, reducing deployment-specific issues.

**This reduces surprises when promoting changes.** Code that works in staging will work in production because environments are consistent.

***

## Quality Attributes Summary

The table separates what is **implemented today** from patterns presented as **teaching models / aspirational** so the mechanisms aren't read as all-adopted.

| Attribute           | Implemented today                                                                                                                                                            | Illustrative / aspirational                                                      | Primary Benefit                 |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------- |
| **Maintainability** | App + feature-folder boundaries, shared Supabase + `@tuturuuu/types/db`, shared `packages/internal-api` / `packages/apis`, modular Next.js frontend, per-app Vercel projects | Hexagonal ports/adapters, repository interfaces, technology-swap seams           | Sustainable long-term evolution |
| **Testability**     | Vitest unit + colocated `*.test.ts`, Zod payload validation, Turborepo affected-package CI                                                                                   | Pure domain objects, in-memory test doubles, formal contract tests, event replay | High confidence in changes      |
| **Deployability**   | Per-app Vercel preview/deploy workflows, separate Rust `apps/backend` track, pinned toolchain (`bun@1.3.14`, Node `>=22`), Supabase migrations + typegen                     | Canary/blue-green orchestration scripts, custom feature-flag service             | Frequent, low-risk releases     |

<Note>
  These properties are being preserved through the `apps/web` → `apps/tanstack-web` + Rust `apps/backend` migration. See [TanStack + Rust Migration](/platform/architecture/tanstack-rust-migration).
</Note>

## Related Documentation

* [Core Architectural Decisions](/platform/architecture/system-design/architectural-decisions) - Why we chose this architecture
* [Layering Patterns](/platform/architecture/system-design/layering-patterns) - N-tier vs Hexagonal/Clean architecture
* [Hexagonal Architecture](/platform/architecture/system-design/hexagonal-architecture) - Ports and adapters in detail
* [Microservices Patterns](/platform/architecture/system-design/microservices-patterns) - Service boundaries and deployment
* [Event-Driven Architecture](/platform/architecture/system-design/event-driven-architecture) - Event streaming patterns
* [Observability & Monitoring](/platform/architecture/system-design/observability-monitoring) - Testing in production
