Skip to main content
Tuturuuu uses a monorepo of conventional Next.js App Router apps that share one Supabase project plus a growing set of workspace packages. We describe this as a “microservices-within-a-monorepo” style: each app deploys independently and owns a product surface, but they are not isolated services with private databases or an inter-service token mesh. This page documents the patterns we actually use today, and flags where the platform is mid-migration.
Active migration. The legacy apps/web Next.js runtime (port 7803) is being replaced by apps/tanstack-web (TanStack Start) for the frontend and apps/backend (Rust) for the API layer. See TanStack Start And Rust Migration for the migration contract and apps/tanstack-web/migration/route-manifest.json for live route-by-route progress. Treat the tRPC and Next.js REST sections below as the legacy apps/web path; new read/write endpoints increasingly land in the Rust backend.

Service Boundaries

Current Services

ServiceLocationPurposeDatabaseTech Stack
web (legacy)apps/webMain platform application (port 7803)Supabase (PostgreSQL)Next.js (App Router), React 19
tanstack-webapps/tanstack-webReplacement frontend for apps/webSupabase (shared)TanStack Start + Router + Query, Vite, React 19
backendapps/backendDedicated Rust API runtime (port 7820)Supabase (shared)Rust (Axum), Cloudflare Workers-ready
rewiseapps/rewiseAI-powered chatbotSupabase (shared)Next.js, AI SDK
novaapps/novaPrompt engineering platformSupabase (shared)Next.js, AI SDK
calendarapps/calendarCalendar & schedulingSupabase (shared)Next.js, React
financeapps/financeFinance managementSupabase (shared)Next.js, React
tasksapps/tasksTask management (hierarchical)Supabase (shared)Next.js, React
meetapps/meetMeeting managementSupabase (shared)Next.js, React
shortenerapps/shortenerURL shortener serviceSupabase (shared)Next.js
databaseapps/databaseDatabase migrations & schemaSupabaseSQL, TypeScript
discordapps/discordDiscord bot utilitiesNone (stateless)Python
Additional product apps (apps/chat, apps/drive, apps/mail, apps/cms, apps/mind, apps/teach, apps/track, apps/storefront, and others) follow the same conventional Next.js App Router shape and share the same Supabase project.
All these apps share one Supabase project. There are no per-app distinct Supabase keys, no per-service databases, and no inter-service token system in the codebase today. “Service-specific databases” and an inter-service token mesh are aspirational patterns discussed later, not implemented reality.

Shared Packages

All services share common infrastructure via workspace packages:
packages/
├── ui/           # Shared UI components (Shadcn)
├── ai/           # AI integration utilities
├── supabase/     # Supabase client wrappers
├── types/        # Shared TypeScript types (including generated DB types)
├── utils/        # Cross-cutting utilities
├── trigger/      # Background job definitions (Trigger.dev v4)
├── payment/      # Payment processing (Polar provider)
├── internal-api/ # Typed helpers for app REST endpoints
└── ... (many shared packages)

Monorepo Architecture

Benefits

  1. Shared Code: Common utilities, UI components, types shared across services
  2. Atomic Changes: Change types in one commit, update all services
  3. Simplified Tooling: Single build system (Turborepo + Bun)
  4. Easy Refactoring: Move code between services, extract packages
  5. Consistent Standards: Shared linting, testing, deployment configs

Structure

platform/
├── apps/                  # Independent microservices
│   ├── web/              # Main app (port 7803)
│   ├── rewise/           # AI chat
│   ├── nova/             # Prompt engineering
│   └── ... (other apps)
├── packages/              # Shared libraries
│   ├── ui/               # Component library
│   ├── types/            # Shared types
│   └── ... (other packages)
├── turbo.json            # Turborepo configuration
└── package.json          # Workspace root

Workspace Dependencies

Services declare dependencies on shared packages:
// apps/web/package.json
{
  "name": "@tuturuuu/web",
  "dependencies": {
    "@tuturuuu/ui": "workspace:*",
    "@tuturuuu/types": "workspace:*",
    "@tuturuuu/supabase": "workspace:*",
    "@tuturuuu/ai": "workspace:*",
    "@tuturuuu/utils": "workspace:*"
  }
}

// apps/rewise/package.json
{
  "name": "@tuturuuu/rewise",
  "dependencies": {
    "@tuturuuu/ui": "workspace:*",
    "@tuturuuu/types": "workspace:*",
    "@tuturuuu/ai": "workspace:*"
    // Note: Different subset of packages
  }
}

Communication Patterns

1. Background Jobs (Trigger.dev v4)

When to use: Asynchronous workflows, background processing, scheduled work Implementation: Trigger.dev v4 (@trigger.dev/sdk ^4.4.5), using the task() API. Task definitions live in packages/trigger/src. The v2/v3 client.defineJob / eventTrigger / io.runTask APIs do not exist in this repo. Define a task with task() and run it from app code with tasks.trigger(...) (or <task>.trigger(...)):
// packages/trigger/src/schedule-tasks.ts
import { task } from '@trigger.dev/sdk/v3';
import { schedulableTasksHelper } from './schedule-tasks-helper';

export const scheduleTask = task({
  id: 'schedule-task',
  queue: { concurrencyLimit: 10 },
  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 };
  },
});
// Producer: trigger the task from app code (e.g. an apps/web API route)
import { tasks } from '@trigger.dev/sdk/v3';
import type { scheduleTask } from '@tuturuuu/trigger';

await tasks.trigger<typeof scheduleTask>('schedule-task', {
  ws_id: workspaceId,
});
The import path is @trigger.dev/sdk/v3 even though the installed SDK is the v4 line — that path is the current entrypoint for the task() API.
Characteristics:
  • Loose coupling: producers fire-and-forget, tasks run out of band
  • Resilient to failures, with built-in retries and queue concurrency limits
  • Asynchronous processing
  • Run history and replay in the Trigger.dev dashboard

2. Shared Database (Current)

When to use: Strong consistency requirements, complex queries across entities Implementation: Supabase PostgreSQL with RLS
import { createClient } from '@tuturuuu/supabase/next/server';

// Both apps/web and apps/finance access the same Supabase project.
// createClient() is async — await it.
const supabase = await createClient();

// Row-Level Security ensures data isolation
const { data } = await supabase
  .from('workspaces')
  .select('*')
  .eq('id', workspaceId);
// RLS automatically filters by user permissions
Characteristics:
  • Strong consistency
  • ACID transactions
  • Complex joins possible
  • Shared schema (requires coordination)
Trade-offs:
  • ✅ Simple implementation
  • ✅ Strong consistency
  • ❌ Tight coupling at data layer
  • ❌ Schema changes affect multiple services

3. tRPC (legacy apps/web, currently a stub)

Status: The tRPC layer in apps/web/src/trpc is intentionally a stub. The router (apps/web/src/trpc/routers/_app.ts) exports a single healthCheck baseProcedure to keep the type system wired up:
// apps/web/src/trpc/routers/_app.ts (actual contents)
import { baseProcedure, createTRPCRouter } from '../init';

export const appRouter = createTRPCRouter({
  healthCheck: baseProcedure.query(() => {
    return { status: 'ok', timestamp: Date.now() };
  }),
});

export type AppRouter = typeof appRouter;
The real exports are createTRPCRouter, createTRPCContext, baseProcedure, and createCallerFactory. There is no protectedProcedure, no auth middleware, and no workspace/user/tasks/ai routers — the context is not a product data layer. Product data flows through @tuturuuu/internal-api helpers and REST /api/v1/* routes (below), not through tRPC. In apps/tanstack-web a guard (check-tanstack-api-access) forbids /trpc calls entirely. Do not document tRPC as the canonical internal API surface — it is a thin compatibility shim that may be removed during the migration.

4. REST API (/api/v1, legacy apps/web) and the Rust backend

When to use: Public APIs, webhook endpoints, third-party integrations, and product read/write endpoints. Legacy implementation: Next.js App Router route handlers under apps/web/src/app/api/v1/* (e.g. workspaces, inventory, nova, storage):
// apps/web/src/app/api/v1/workspaces/route.ts
import { createClient } from '@tuturuuu/supabase/next/server';

export async function POST(request: Request) {
  const supabase = await createClient(); // async — await it
  const body = await request.json();

  // Validate, authenticate, execute
  const workspace = await createWorkspace(supabase, body);

  return Response.json({ workspace }, { status: 201 });
}
Migration target: The Rust backend (apps/backend, port 7820) is progressively taking ownership of these endpoints. Route groups live in apps/backend/src/*.rs (for example inventory.rs, nova.rs, onboarding_progress.rs, aurora.rs), and the contract is documented in apps/backend/api/openapi.yaml. Migration progress per endpoint is tracked in apps/tanstack-web/migration/route-manifest.json. Characteristics:
  • Standard HTTP / JSON
  • Versioned endpoints (/api/v1/...)
  • Rate limiting and authentication
  • OpenAPI documentation for the Rust backend (apps/backend/api/openapi.yaml)

Service Communication Matrix

FromToPatternProtocolUse Case
any appSupabaseShared DBPostgreSQL + RLSRead/write product data (primary path)
any appTrigger.devBackground jobtask() triggerAsync/scheduled work
ExternalappREST APIHTTP/JSONPublic API access (/api/v1/...)
tanstack-webbackendRESTHTTP/JSONMigrated read/write endpoints (Rust)
app (client)app (server)TanStack QueryHTTP/JSONClient fetching via internal-api helpers
discordwebWebhookHTTP/JSONDiscord bot commands
Most cross-surface coordination is not direct service-to-service RPC: apps share the Supabase database (guarded by RLS) and hand off long-running work to Trigger.dev. This keeps coupling at the data and job layers rather than in a service mesh.

Deployment Strategies

Next.js apps: Vercel

Each Next.js app deploys independently to Vercel. CI builds (and validates) each app; the actual production deploy is handled by Vercel’s Git integration, not by a vercel deploy step in the workflow. There is one Vercel workflow per app, named by product surface. For example, vercel-production-platform.yaml builds apps/web on pushes to the production branch (build-only — the vercel deploy step was intentionally removed):
# .github/workflows/vercel-production-platform.yaml (abridged)
name: Vercel Platform Production Deployment
on:
  push:
    branches: [production]
  workflow_dispatch:

jobs:
  Deploy-Production:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v6
        with:
          node-version: 24
      - uses: ./.github/actions/setup-bun-with-retry
        with:
          bun-version: 1.3.14
      - run: bash scripts/ci/run-with-backoff.sh bun install
      # Pull env + build the Vercel artifact; deploy happens via Vercel Git integration.
      - run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
      - run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
Matching vercel-preview-*.yaml workflows run for pull requests. Node engines are >=22 (CI uses Node 24) and Bun is pinned to 1.3.14.

Rust backend

apps/backend is built and validated by .github/workflows/rust-backend.yml. It runs as a native container and keeps a Cloudflare Workers Rust entrypoint (apps/backend/wrangler.jsonc) ready for edge preview deployment, per the migration contract.

Independent Scaling

Each app scales independently on its hosting platform (Vercel autoscaling for the Next.js apps; container/edge scaling for the Rust backend). The per-app minInstances/maxInstances objects shown in earlier versions of this page were illustrative, not a real config file in the repo — scaling is configured in the Vercel project settings and the backend’s container/edge runtime, not in versioned source.

Service Boundary Decisions

When to Create a New Service

Extract to new service when:
  • Feature is logically independent (e.g., URL shortener)
  • Different scaling requirements (high-traffic vs low-traffic)
  • Different technology needs (Python for ML vs TypeScript for web)
  • Team ownership boundary (separate team owns feature)
  • Independent deployment cycle needed
Keep in existing service when:
  • Shares most code with existing service
  • Tight coupling to core domain
  • Low complexity (< 1000 LOC)
  • No special scaling or technology needs

Example: Why finance is a separate app

Reasons:
✅ Logically independent domain (finance vs tasks)
✅ Can be worked on by dedicated team
✅ Independent deployment for finance updates
✅ Future: Could use different database for financial data

Alternatives considered:
❌ Module in web app - harder to maintain boundaries
❌ Separate package - loses deployment independence

Data Ownership

Current: Shared Database Pattern

Pros:
  • Simple joins across entities
  • Strong consistency
  • ACID transactions
  • Single source of truth
Cons:
  • Tight coupling at data layer
  • Schema changes affect multiple services
  • Harder to scale independently

Aspirational: Service-Specific Databases

This is not implemented today. Every app shares one Supabase project. The following is a future option, not current architecture.
When to consider:
  • A surface needs a specialized database (e.g., PostGIS for geolocation)
  • Independent scaling requirements for specific data
  • Strong service boundaries needed
Illustrative pattern (not in the codebase):
import { createClient } from '@tuturuuu/supabase/next/server';

// Today: every app uses the shared Supabase project.
const db = await createClient(); // createClient() is async — await it

// Hypothetical future: a dedicated DB plus a background task to keep
// derived data in sync, triggered via Trigger.dev's task() API.
// await tasks.trigger('record-transaction', { /* ... */ });

Package Extraction Strategy

When to Extract to Package

The repo’s root AGENTS.md carries the package-extraction decision matrix. As a rough heuristic, extract when ≥3 HIGH signals:
SignalExtract Now (HIGH)
Reuse BreadthActively duplicated in ≥2 apps
Complexity>150 LOC multi-module
Domain OwnershipPure cross-domain utility
Testing NeedsComprehensive tests stable
Example extractions in Tuturuuu:
@tuturuuu/ui → Shared across all apps
@tuturuuu/types → Used by all services
@tuturuuu/supabase → Client wrapper used everywhere
@tuturuuu/ai → Shared AI utilities (rewise, nova, web)

Testing Strategies

Unit Tests

Test individual services in isolation:
// apps/web/src/__tests__/workspace.test.ts
import { createWorkspace } from '../lib/workspace';

describe('Workspace Service', () => {
  it('creates workspace with valid data', async () => {
    const workspace = await createWorkspace({
      ownerId: 'user-1',
      name: 'Test Workspace'
    });

    expect(workspace.name).toBe('Test Workspace');
  });
});

Integration Tests

Test service interactions:
// apps/web/src/__tests__/integration/workspace-creation.test.ts
// Tests run under Vitest. There is no event-store to assert against;
// instead, mock the Trigger.dev task trigger and assert it was invoked.
import { vi } from 'vitest';
import { tasks } from '@trigger.dev/sdk/v3';

vi.spyOn(tasks, 'trigger');

describe('Workspace Creation Flow', () => {
  it('creates a workspace and schedules follow-up work', async () => {
    const workspace = await POST('/api/v1/workspaces', {
      name: 'Test Workspace',
    });

    expect(tasks.trigger).toHaveBeenCalledWith(
      'schedule-task',
      expect.objectContaining({ ws_id: workspace.id })
    );
  });
});

End-to-End Tests

Test full user workflows across services (future):
// e2e/workspace-onboarding.spec.ts
test('user creates workspace and receives AI setup', async ({ page }) => {
  await page.goto('/workspaces/create');
  await page.fill('[name="name"]', 'Test Workspace');
  await page.click('button[type="submit"]');

  // Wait for AI setup completion
  await page.waitForSelector('[data-testid="ai-ready"]');
});

Monitoring & Observability

Service Health

// apps/web/src/app/api/health/route.ts
export async function GET() {
  const health = {
    service: 'web',
    status: 'healthy',
    uptime: process.uptime(),
    dependencies: {
      database: await checkDatabaseConnection(),
      eventBroker: await checkTriggerConnection()
    }
  };

  return Response.json(health);
}

Correlation IDs

There is no dedicated @tuturuuu/logging or @tuturuuu/observability package and no event-store; the example below is an illustrative pattern. Note that apps/web server-side code must route logs through the internal log-drain logger rather than raw console.* calls.
import { tasks } from '@trigger.dev/sdk/v3';

export async function POST(request: Request) {
  const correlationId =
    request.headers.get('x-correlation-id') ?? crypto.randomUUID();

  // Hand off async work to a Trigger.dev task, threading the correlation id.
  await tasks.trigger('schedule-task', { ws_id: workspaceId, correlationId });

  // Use the internal log-drain logger in apps/web server code (not console.*).
}

Migration Paths

Current State → Future State

Current: A monorepo of Next.js apps over one shared Supabase project, with background work on Trigger.dev v4. In progress: The frontend is migrating from apps/web (Next.js) to apps/tanstack-web (TanStack Start), and the API layer to apps/backend (Rust). This is the concrete, active migration — see TanStack Start And Rust Migration. Longer-term options (not committed):
  1. Service-Specific Databases (when needed)
    • Extract finance data to dedicated DB
    • Communicate via events
    • Maintain consistency with sagas
  2. API Gateway Layer (if REST APIs grow)
    • Single entry point for external clients
    • Route to appropriate services
    • Handle auth, rate limiting centrally
  3. GraphQL Federation (if complex queries needed)
    • Each service exposes GraphQL schema
    • Gateway federates schemas
    • Clients query unified graph
Migration principle: Evolve gradually based on real needs, not speculation.