> ## 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.

# Encapsulation Patterns

> Strategies for preventing unwanted communication within and between services in Tuturuuu's architecture

Encapsulation is a critical architectural quality that prevents unwanted coupling and maintains clear boundaries. This document covers patterns for preventing unwanted communication both **within an app** (cross-layer) and **between apps** (cross-service).

<Info>
  For a comprehensive comparison of how different architectural patterns handle encapsulation, see [Architectural Patterns Comparison](/platform/architecture/system-design/architectural-patterns-comparison).
</Info>

<Warning>
  **How to read this page.** The cross-layer hexagonal examples below
  (`domain/`, `application/`, `infrastructure/` directories, `WorkspaceRepository`
  ports, rich `Workspace` domain models) are **illustrative teaching patterns**,
  not a description of the current codebase. Tuturuuu apps are conventional
  **Next.js App Router** trees: `apps/web/src` contains `app/`, `components/`,
  `features/`, `hooks/`, `lib/`, and `utils/` — there is no
  `domain/`/`application/`/`infrastructure/` layering and no separate ports/adapters
  package. Likewise, apps communicate primarily through a **shared Supabase
  database** plus **Trigger.dev v4** background tasks, not through a broker with
  event ACLs. Each "in Tuturuuu" snippet below is marked as either *real* or
  *illustrative*. Treat the illustrative ones as design inspiration you could adopt,
  not as files that exist today.
</Warning>

<Note>
  **Migration in progress.** `apps/web` (Next.js, port 7803) is being replaced by
  `apps/tanstack-web` (TanStack Start) plus `apps/backend` (Rust, port 7820). The
  encapsulation principles here remain valid across both stacks, but treat any
  `apps/web`-specific path as legacy-or-current rather than permanent. See
  [TanStack Start and Rust Migration](/platform/architecture/tanstack-rust-migration).
</Note>

## Preventing Cross-Layer Communication (Inside an App)

Even without formal hexagonal layering, an app benefits from clear internal
boundaries that prevent tight coupling. The patterns below show how dependency
inversion, DTOs, and layered responsibility *could* be applied; where Tuturuuu
already does something equivalent (RLS, package boundaries, `internal-api`
helpers), that is called out as *real*.

### 1. The Dependency Inversion Principle (via Ports)

<Info>
  **Illustrative pattern.** The ports/adapters file paths below
  (`packages/types/src/domain/*`, `apps/web/src/domain/*`,
  `apps/web/src/infrastructure/*`) **do not exist** in the repo today and are shown
  purely to teach the dependency inversion principle. `packages/types/src` exposes
  flat modules (`db.ts`, `index.ts`, `supabase.ts`, `sdk.ts`, …) with no `domain/`
  directory.
</Info>

High-level business logic can define interfaces ("Ports") it requires, and
low-level infrastructure (like database code) implements them.

**Architectural Principle:** High-level modules should not depend on low-level modules. Both should depend on abstractions.

**Illustrative example:**

```typescript theme={null}
// Hypothetical domain layer defines interface (PORT)
// e.g. packages/types/src/domain/workspace.ts (does not exist today)
export interface WorkspaceRepository {
  findById(id: string): Promise<Workspace | null>;
  findByOwnerId(ownerId: string): Promise<Workspace[]>;
  save(workspace: Workspace): Promise<void>;
  delete(id: string): Promise<void>;
}

// Business logic depends on abstraction, NOT concrete implementation
// e.g. apps/web/src/domain/services/workspace-service.ts (illustrative path)
export class WorkspaceService {
  constructor(
    private readonly repository: WorkspaceRepository  // Depends on interface
  ) {}

  async createWorkspace(ownerId: string, name: string): Promise<Workspace> {
    const workspace = new Workspace(ownerId, name);

    // Domain logic doesn't know about Supabase, PostgreSQL, etc.
    await this.repository.save(workspace);

    return workspace;
  }
}

// Infrastructure layer implements interface (ADAPTER)
// e.g. apps/web/src/infrastructure/repositories/supabase-workspace-repository.ts (illustrative path)
export class SupabaseWorkspaceRepository implements WorkspaceRepository {
  constructor(private readonly supabase: SupabaseClient) {}

  async findById(id: string): Promise<Workspace | null> {
    const { data } = await this.supabase
      .from('workspaces')
      .select('*')
      .eq('id', id)
      .single();

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

  async save(workspace: Workspace): Promise<void> {
    await this.supabase
      .from('workspaces')
      .upsert(this.toDatabaseModel(workspace));
  }

  // ... other methods
}
```

**Benefits:**

* Domain logic is **pure** and **testable** without infrastructure
* Can **swap implementations** (Supabase → Drizzle → Prisma) without changing business logic
* **Inversion of control** - domain doesn't depend on infrastructure
* **Technology agnostic** core business logic

**Clarifying Additions:** This enforces a clean separation that preserves the integrity of business rules. Infrastructure choices become replaceable rather than deeply embedded. The architecture stays flexible as technical needs evolve.

**"Anti-pattern" — but also how Tuturuuu actually writes code today:**

```typescript theme={null}
// In the hexagonal sense this "couples business logic to Supabase".
// In practice this IS the current Tuturuuu pattern: route handlers and
// server components call the Supabase client directly.
import { createClient } from '@tuturuuu/supabase/next/server';

export async function createWorkspace(ownerId: string, name: string) {
  // NOTE: createClient() from @tuturuuu/supabase/next/server is ASYNC — await it.
  // (createAdminClient() is synchronous; createDynamicClient() is async.)
  const supabase = await createClient();

  const { data } = await supabase
    .from('workspaces')
    .insert({ owner_id: ownerId, name })
    .select()
    .single();

  return data;
}
```

The hexagonal critique (harder to unit-test, technology lock-in) is real, but
Tuturuuu deliberately accepts direct Supabase access for app/API code and relies
on **Row-Level Security**, **shared `@tuturuuu/supabase` wrappers**, and
**`packages/internal-api` helpers** for boundaries instead of a ports/adapters
layer. Adopt the repository pattern only where a unit of logic genuinely needs to
be storage-agnostic or heavily unit-tested.

***

### 2. Strict Data Transfer Objects (DTOs)

The outer API layer of a service communicates with its inner Application layer using plain DTOs. This creates a strong boundary that prevents internal, behavior-rich Domain Models from being exposed to external layers, ensuring the core logic remains fully encapsulated.

**Architectural Principle:** External layers communicate via simple data structures, not rich domain objects.

<Info>
  **Illustrative pattern.** The rich `Workspace` domain class and
  `apps/web/src/domain/models/workspace.ts` path are illustrative. Real
  `apps/web` API routes work with plain objects and generated DB row types from
  `@tuturuuu/types/db`, not behavior-rich domain entities. The DTO discipline (keep
  HTTP request/response shapes separate from internal models, serialize dates as
  ISO strings) is still worth applying.
</Info>

**Illustrative example:**

```typescript theme={null}
// API layer DTO (external contract)
// e.g. apps/web/src/app/api/workspaces/create/route.ts
interface CreateWorkspaceRequest {
  name: string;
  description?: string;
}

interface CreateWorkspaceResponse {
  id: string;
  name: string;
  ownerId: string;
  createdAt: string;  // Serialized as ISO string
}

export async function POST(request: Request) {
  const body: CreateWorkspaceRequest = await request.json();

  // Convert DTO to domain command
  const command = new CreateWorkspaceCommand(
    userId,
    body.name,
    body.description
  );

  // Execute in domain
  const workspace = await workspaceService.execute(command);

  // Convert domain model back to DTO
  const response: CreateWorkspaceResponse = {
    id: workspace.id,
    name: workspace.name,
    ownerId: workspace.ownerId,
    createdAt: workspace.createdAt.toISOString()  // Domain Date → DTO string
  };

  return Response.json(response);
}

// Domain model (internal, rich behavior) — illustrative; no such file exists today
// e.g. apps/web/src/domain/models/workspace.ts
export class Workspace {
  constructor(
    public readonly id: string,
    public readonly ownerId: string,
    private _name: string,
    private _isActive: boolean,
    public readonly createdAt: Date
  ) {}

  // Rich domain behavior
  rename(newName: string) {
    if (newName.length < 3) {
      throw new DomainError('Workspace name must be at least 3 characters');
    }
    this._name = newName;
  }

  activate() {
    if (this._isActive) {
      throw new DomainError('Workspace is already active');
    }
    this._isActive = true;
  }

  deactivate() {
    this._isActive = false;
  }

  get name() {
    return this._name;
  }

  get isActive() {
    return this._isActive;
  }
}
// Domain model NEVER exposed to API layer
// API only sees DTOs
```

**Benefits:**

* **Encapsulation** of domain behavior
* **Clear boundaries** between layers
* **API stability** - internal changes don't break API contract
* **Serialization control** - DTOs are JSON-friendly
* **Versioning** - can support multiple DTO versions

**Clarifying Additions:** Interfaces clarify what the domain requires without exposing internal structures. This allows external systems to interact without knowing implementation details. Such boundaries minimize coupling between layers.

**Anti-pattern to avoid:**

```typescript theme={null}
// BAD: Exposing domain model directly
export async function POST(request: Request) {
  const workspace = await workspaceService.create(data);

  // WRONG: Returning domain model directly
  return Response.json(workspace);
}
// Problems:
// - Domain behavior exposed (rename, activate methods in JSON)
// - Date objects don't serialize correctly
// - Internal structure leaked to external API
// - Can't evolve domain without breaking API
```

***

### 3. Explicit Layered Responsibility

The architecture enforces a clear separation of concerns. The **Presentation layer** handles HTTP, the **Application layer** orchestrates workflows, and the **Domain layer** contains pure business logic. This clarity prevents logic from being misplaced and ensures layers only interact through their well-defined public interfaces.

**Architectural Principle:** Each layer has a single, well-defined responsibility and communicates only through defined interfaces.

**Actual `apps/web/src` structure (real):**

```
apps/web/src/
├── app/            # Next.js App Router: pages, layouts, and api/ route handlers
├── components/     # Shared React components for this app
├── features/       # Feature-scoped UI + logic
├── hooks/          # React hooks
├── lib/            # App-level helpers and clients
├── utils/          # Cross-cutting utilities
├── context/        # React context providers
├── data/           # Static/seed data
├── trpc/           # Local tRPC stub (healthCheck only — not a product data layer)
└── i18n/           # Localization config
```

There is **no** `application/`, `domain/`, or `infrastructure/` directory. Cross-app
product data flows through the **shared Supabase database** and
**`packages/internal-api`** helpers calling REST `/api/v1` routes — not through a
ports/adapters layer.

**Illustrative hexagonal structure** (what an explicitly layered service *could*
look like — not present today):

```
src/
├── app/                          # Presentation Layer (HTTP) — real today
│   └── api/workspaces/create/route.ts
├── application/                  # Application Layer (Orchestration) — illustrative
│   └── use-cases/create-workspace.ts
├── domain/                       # Domain Layer (Business Logic) — illustrative
│   ├── models/workspace.ts
│   └── services/workspace-service.ts
└── infrastructure/               # Infrastructure Layer (Technical) — illustrative
    └── repositories/supabase-workspace-repository.ts
```

**Illustrative example with clear layer responsibilities:**

```typescript theme={null}
// PRESENTATION LAYER: HTTP concerns only
// apps/web/src/app/api/workspaces/create/route.ts
export async function POST(request: Request) {
  // 1. HTTP handling (validation, auth)
  const session = await getSession();
  if (!session) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const body = await request.json();

  // Validate input schema
  const validation = CreateWorkspaceSchema.safeParse(body);
  if (!validation.success) {
    return Response.json({ error: validation.error }, { status: 400 });
  }

  // 2. Delegate to application layer
  const useCase = new CreateWorkspaceUseCase(
    workspaceRepository,
    eventPublisher
  );

  try {
    const workspace = await useCase.execute({
      ownerId: session.userId,
      name: validation.data.name,
      description: validation.data.description
    });

    // 3. HTTP response
    return Response.json({ workspace }, { status: 201 });
  } catch (error) {
    return Response.json({ error: error.message }, { status: 500 });
  }
}

// APPLICATION LAYER: Workflow orchestration
// apps/web/src/application/use-cases/create-workspace.ts
export class CreateWorkspaceUseCase {
  constructor(
    private readonly repository: WorkspaceRepository,
    private readonly eventPublisher: EventPublisher
  ) {}

  async execute(command: CreateWorkspaceCommand): Promise<Workspace> {
    // 1. Create domain entity
    const workspace = Workspace.create(
      command.ownerId,
      command.name,
      command.description
    );

    // 2. Persist
    await this.repository.save(workspace);

    // 3. Publish domain event
    await this.eventPublisher.publish(
      new WorkspaceCreatedEvent(workspace.id, workspace.ownerId)
    );

    return workspace;
  }
}

// DOMAIN LAYER: Pure business logic
// apps/web/src/domain/models/workspace.ts
export class Workspace {
  static create(
    ownerId: string,
    name: string,
    description?: string
  ): Workspace {
    // Business rules
    if (name.length < 3 || name.length > 50) {
      throw new DomainError('Workspace name must be 3-50 characters');
    }

    return new Workspace(
      generateId(),
      ownerId,
      name,
      description,
      true,  // isActive
      new Date()
    );
  }

  // No HTTP, no database, no events - pure domain logic
}
```

**Layer Communication Rules:**

| From Layer     | Can Call                | Cannot Call                               | Reason                 |
| -------------- | ----------------------- | ----------------------------------------- | ---------------------- |
| Presentation   | Application             | Domain directly                           | Bypass orchestration   |
| Application    | Domain, Infrastructure  | Presentation                              | Circular dependency    |
| Domain         | Nothing                 | Application, Infrastructure, Presentation | Pure logic             |
| Infrastructure | Domain (via interfaces) | Application, Presentation                 | Implementation details |

**Benefits:**

* **Clear responsibility** per layer
* **Easy to reason about** where code belongs
* **Testability** - each layer tested independently
* **Maintainability** - changes confined to appropriate layer

**Clarifying Additions:** This keeps the heart of the system stable despite environmental changes. The domain stays consistent across different execution contexts. This significantly boosts reliability and predictability.

**Anti-pattern to avoid:**

```typescript theme={null}
// BAD: Mixed responsibilities
export async function POST(request: Request) {
  const body = await request.json();

  // WRONG: Business logic in API layer
  if (body.name.length < 3) {
    return Response.json({ error: 'Name too short' }, { status: 400 });
  }

  // WRONG: Direct database access from API layer
  const { data } = await supabase
    .from('workspaces')
    .insert({ name: body.name });

  // WRONG (and outdated API): firing a background task inline with no structure.
  // In Trigger.dev v4 you trigger a defined task, e.g.:
  //   await myTask.trigger({ workspaceId: data.id });
  // (Trigger.dev v2 APIs like trigger.event/client.defineJob do not exist in this repo.)

  return Response.json(data);
}
// Problems:
// - Business rules scattered across codebase
// - No single source of truth for validation
// - Hard to test
// - Difficult to change
```

***

## Preventing Unwanted Cross-Service Communication (Between Apps)

<Warning>
  **Reality check.** Tuturuuu apps do **not** communicate exclusively through
  published events, and there is **no broker-level event ACL**. The current primary
  inter-app pattern is a **shared Supabase (PostgreSQL) database** with Row-Level
  Security, complemented by **Trigger.dev v4** tasks for asynchronous/background
  work. This matches the
  [Microservices Patterns](/platform/architecture/system-design/microservices-patterns)
  page, which lists "Shared Database (Current)" as the live pattern. The
  event-as-sole-interface material below is therefore an **aspirational pattern**
  showing the encapsulation benefits a fully event-driven design would provide —
  not the way the apps interact today.
</Warning>

### 1. Background Tasks as a Decoupling Boundary

When one app needs to kick off work in another context without a synchronous
dependency, Tuturuuu uses **Trigger.dev v4 `task()`** definitions in
`packages/trigger/src`. A trigger payload acts as a small, explicit contract: the
caller passes only what the task needs, and the task does not reach back into the
caller's internals.

**Architectural Principle:** Cross-context work is dispatched via explicit task payloads, not by calling another app's private functions.

**Real example (Trigger.dev v4):**

```typescript theme={null}
// packages/trigger/src/schedule-tasks.ts (real, abridged)
import { task } from '@trigger.dev/sdk/v3';
import { schedulableTasksHelper } from './schedule-tasks-helper';

export const scheduleTask = task({
  id: 'schedule-task',
  queue: { concurrencyLimit: 10 },
  // The payload is the entire contract this task depends on.
  run: async (payload: { ws_id: string }) => {
    const result = await schedulableTasksHelper(payload.ws_id);
    if (!result.success) {
      throw new Error(result.error || 'Schedule tasks failed');
    }
    return { ws_id: payload.ws_id, ...result, success: true };
  },
});
```

```typescript theme={null}
// Dispatching the task from app code (Trigger.dev v4):
import { scheduleTask } from '@tuturuuu/trigger';

// Fire-and-forget; returns a run handle, does not block on completion.
await scheduleTask.trigger({ ws_id: workspaceId });
```

<Note>
  The import path is `@trigger.dev/sdk/v3` even though the installed package is
  `@trigger.dev/sdk@^4.4.5` — the `/v3` subpath is the v4 SDK's stable entrypoint
  for `task()`. The Trigger.dev **v2** APIs referenced in older drafts
  (`client.defineJob`, `eventTrigger`, `io.runTask`, `trigger.event`) **do not
  exist** in this repo.
</Note>

***

### 2. (Aspirational) Published Event Contracts as the Sole Interface

In a fully event-driven design, the only way services interact is through the
events they publish; internal implementation, database schema, and private
functions stay **completely hidden**. Tuturuuu does **not** implement this today
(apps share a database), but the pattern is the strongest form of cross-service
encapsulation and is worth understanding.

**Architectural Principle:** Services communicate only via well-defined event contracts.

**Aspirational example (expressed in real Trigger.dev v4 syntax):**

```typescript theme={null}
// Producer side: instead of a v2 trigger.event(...) broadcast,
// dispatch a typed task with an explicit payload contract.
import { setupAiWorkspaceTask } from '@tuturuuu/trigger';

export async function createWorkspace(data: CreateWorkspaceData) {
  const supabase = await createClient(); // async client — await it
  const { data: workspace } = await supabase
    .from('workspaces')
    .insert(data)
    .select()
    .single();

  // Public contract = the task payload shape, nothing more.
  await setupAiWorkspaceTask.trigger({
    workspaceId: workspace.id,
    ownerId: workspace.owner_id,
  });
}

// Consumer side (Trigger.dev v4): a task() definition, not client.defineJob.
import { task } from '@trigger.dev/sdk/v3';

export const setupAiWorkspaceTask = task({
  id: 'setup-ai-workspace',
  run: async (payload: { workspaceId: string; ownerId: string }) => {
    // Only knows about the payload contract, not the producer's internals.
    return await createAIConfig(payload);
  },
});
```

**Event Schema Governance (aspirational):**

```typescript theme={null}
// Illustrative: e.g. packages/types/src/events/workspace.ts (no such file today)
import { z } from 'zod';

// Versioned schemas keep payload contracts backward compatible.
export const WorkspaceCreatedEventV1 = z.object({
  workspaceId: z.string().uuid(),
  ownerId: z.string().uuid(),
  name: z.string().min(1).max(100),
  createdAt: z.string().datetime()
});

export const WorkspaceCreatedEventV2 = z.object({
  workspaceId: z.string().uuid(),
  ownerId: z.string().uuid(),
  name: z.string().min(1).max(100),
  createdAt: z.string().datetime(),
  // New optional fields (backward compatible)
  description: z.string().optional(),
  plan: z.enum(['free', 'pro', 'enterprise']).optional()
});

export type WorkspaceCreatedEventV1 = z.infer<typeof WorkspaceCreatedEventV1>;
export type WorkspaceCreatedEventV2 = z.infer<typeof WorkspaceCreatedEventV2>;
```

**Benefits:**

* **Complete encapsulation** of service internals
* **Clear API contract** via event schemas
* **Versioning support** for evolution
* **Independent deployment** of services
* **No direct dependencies** between services

**Clarifying Additions:** Clients remain simple and unaffected by internal changes. Service boundaries stay intact because external access is tightly controlled. This enforces a clean separation between client-facing and internal concerns.

**Anti-pattern to avoid:**

```typescript theme={null}
// BAD: Direct service-to-service HTTP calls
export async function createWorkspace(data: CreateWorkspaceData) {
  const workspace = await db.workspaces.insert(data);

  // WRONG: Direct HTTP call to another service
  await fetch('http://rewise-service/api/setup', {
    method: 'POST',
    body: JSON.stringify({ workspaceId: workspace.id })
  });

  // Problems:
  // - Tight coupling to rewise-service
  // - Must know its URL and API
  // - Synchronous dependency (cascading failures)
  // - Hard to add new services
}
```

***

### 3. (Hypothetical) Broker-Level Access Control (ACLs)

<Warning>
  **Hypothetical pattern — not implemented.** There is **no message broker** and
  **no event ACL system** in Tuturuuu. The file `packages/trigger/src/events/acl.ts`
  and the `EVENT_PRODUCERS`/`EVENT_CONSUMERS` maps below **do not exist** —
  `packages/trigger/src` contains task definitions (`schedule-tasks.ts`,
  `google-calendar-sync.ts`, etc.), not an ACL layer. This section is kept only to
  illustrate what infrastructure-enforced boundaries would look like if Tuturuuu
  moved to a true event-driven broker.
</Warning>

In a broker-based design you would not rely on trust; the broker itself enforces
communication boundaries. Using ACLs you could declare that only one service may
produce `user.registered` events, and only specific downstream services may
consume them.

**Architectural Principle:** Enforce service boundaries at the infrastructure level, not just by convention.

**Hypothetical example (does not reflect repo code):**

```typescript theme={null}
// Hypothetical: packages/trigger/src/events/acl.ts (no such file exists)
export const EVENT_PRODUCERS = {
  'user.registered': ['apps/web'],
  'workspace.created': ['apps/web'],
  'payment.processed': ['apps/web', 'apps/finance'],
  'ai.chat.message': ['apps/rewise'],
  'task.created': ['apps/tasks']
} as const;

export const EVENT_CONSUMERS = {
  'user.registered': ['apps/rewise', 'apps/web', 'analytics-service'],
  'workspace.created': ['apps/rewise', 'apps/nova', 'apps/finance'],
  'payment.processed': ['apps/web', 'billing-service'],
  'ai.chat.message': ['apps/web'],
  'task.created': ['apps/web', 'apps/calendar']
} as const;

// Validation middleware
export function validateEventProducer(
  eventName: string,
  serviceId: string
): boolean {
  const allowedProducers = EVENT_PRODUCERS[eventName];
  if (!allowedProducers) {
    throw new Error(`Unknown event: ${eventName}`);
  }
  if (!allowedProducers.includes(serviceId)) {
    throw new Error(
      `Service ${serviceId} not allowed to produce ${eventName}`
    );
  }
  return true;
}
```

**Benefits:**

* **Enforced boundaries** at infrastructure level
* **Security** - services can't spoof events from other services
* **Audit trail** - know exactly which services produce/consume events
* **Documentation** - ACL config documents allowed communication

**Clarifying Additions:** This ensures services evolve internally without breaking others. External dependencies rely on stable contracts rather than hidden details. This strengthens overall modularity across the architecture.

***

### 4. Elimination of Synchronous Coupling

Dispatching background work asynchronously avoids direct, synchronous calls
between contexts for slow or non-critical flows. This prevents the tight coupling
that arises when one caller must know the network location, API signature, and
live availability of another. Tuturuuu applies this with **Trigger.dev v4** tasks:
the request returns immediately while follow-up work runs out of band.

**Architectural Principle:** Don't block a user request on slow, non-critical downstream work.

**Async flow (real Trigger.dev v4 syntax):**

```typescript theme={null}
import { createAdminClient } from '@tuturuuu/supabase/next/server';
import { setupUserAiTask, sendWelcomeEmailTask } from '@tuturuuu/trigger';

// apps/web: User registers
export async function registerUser(data: UserRegistrationData) {
  const supabase = createAdminClient(); // SYNC factory — do NOT await it
  const user = await createUser(supabase, data);

  // Dispatch background tasks (non-blocking); each returns a run handle.
  await setupUserAiTask.trigger({ userId: user.id });
  await sendWelcomeEmailTask.trigger({ email: user.email });

  return user;  // Returns immediately
}

// packages/trigger/src: each consumer is a task() definition.
import { task } from '@trigger.dev/sdk/v3';

export const setupUserAiTask = task({
  id: 'setup-user-ai',
  run: async (payload: { userId: string }) => {
    return await createAIProfile(payload.userId);
  },
});

export const sendWelcomeEmailTask = task({
  id: 'send-welcome-email',
  run: async (payload: { email: string }) => {
    return await sendEmail(payload.email, 'welcome');
  },
});

// Each task runs independently; registration never blocks on them.
```

**Benefits:**

* **No cascading failures**
* **Independent availability** of services
* **Faster user responses**
* **Easier to add new services**

**Clarifying Additions:** Controlled communication prevents accidental or unauthorized interactions. Service boundaries stay clear as the system grows. This improves safety and consistency in distributed communication.

**Anti-pattern to avoid:**

```typescript theme={null}
// BAD: Synchronous service-to-service calls
export async function registerUser(data: UserRegistrationData) {
  const user = await createUser(data);

  // WRONG: Blocking calls to other services
  await rewiseService.setupAI(user.id);        // 2 seconds
  await emailService.sendWelcome(user.email);  // 1 second
  await analyticsService.track(user.id);       // 1 second

  return user;  // User waits 4+ seconds
}
// Problems:
// - User waits for all services
// - If rewise service is down, registration fails
// - Tight coupling to multiple services
```

***

## Encapsulation in Practice

### Workspace Isolation via RLS (real)

This is Tuturuuu's actual primary encapsulation mechanism: because apps share one
Supabase database, **Row-Level Security** — defined by migrations under
`apps/database/supabase/migrations/` — keeps every app inside its permitted data
scope at the database level, regardless of which app issues the query.

```sql theme={null}
-- apps/database/supabase/migrations/
-- Row-Level Security ensures data encapsulation at database level
CREATE POLICY "Users can only see their workspaces"
ON workspaces
FOR SELECT
USING (
  id IN (
    SELECT workspace_id
    FROM workspace_members
    WHERE user_id = auth.uid()
  )
);

-- Services cannot access data outside their scope
-- Enforced at database level, not application level
```

### Package Boundaries (real)

Apps depend on shared `workspace:*` packages and never import another app's
internals. (Add or update these with scoped installs, e.g.
`cd apps/web && bun add <pkg>` — never `bun add --workspace`.)

```jsonc theme={null}
// apps/web/package.json (illustrative subset)
{
  "dependencies": {
    "@tuturuuu/ui": "workspace:*",        // ✅ Allowed
    "@tuturuuu/types": "workspace:*",     // ✅ Allowed
    "@tuturuuu/supabase": "workspace:*"   // ✅ Allowed
    // ❌ Do not import from apps/rewise or apps/finance directly
  }
}
```

## Summary

Status legend: **real** = implemented today; **illustrative/aspirational** =
teaching pattern not implemented in the repo.

| Encapsulation Type           | Pattern                           | Status       | Enforced By                             | Benefit                |
| ---------------------------- | --------------------------------- | ------------ | --------------------------------------- | ---------------------- |
| **Cross-Layer (Internal)**   | Dependency Inversion / ports      | Illustrative | Hexagonal layering (not adopted)        | Testable, flexible     |
| **Cross-Layer (Internal)**   | DTOs                              | Illustrative | Layer boundaries                        | Stable APIs            |
| **Cross-Layer (Internal)**   | Layered Responsibility            | Illustrative | Directory structure (apps/web is flat)  | Clear organization     |
| **Cross-Layer (Internal)**   | Row-Level Security                | Real         | Supabase RLS policies                   | Data scoping at the DB |
| **Cross-Layer (Internal)**   | Package boundaries                | Real         | `workspace:*` deps + Biome/import rules | No cross-app imports   |
| **Cross-Service (External)** | Background task payloads          | Real         | Trigger.dev v4 `task()`                 | Async decoupling       |
| **Cross-Service (External)** | Event Contracts as sole interface | Aspirational | Event schemas                           | Service autonomy       |
| **Cross-Service (External)** | Broker ACLs                       | Hypothetical | Broker config (no broker today)         | Security, governance   |
| **Cross-Service (External)** | Async background work             | Real         | Trigger.dev v4 tasks                    | No cascading failures  |

## Related Documentation

* [Layering Patterns](/platform/architecture/system-design/layering-patterns) - N-tier vs Hexagonal/Clean architecture comparison
* [Hexagonal Architecture](/platform/architecture/system-design/hexagonal-architecture) - Detailed ports and adapters pattern
* [Event-Driven Architecture](/platform/architecture/system-design/event-driven-architecture) - Event-based communication
* [Microservices Patterns](/platform/architecture/system-design/microservices-patterns) - Service boundaries
* [Authorization](/platform/architecture/authorization) - RLS policies and permissions
