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
| Service | Location | Purpose | Database | Tech Stack |
|---|
| web (legacy) | apps/web | Main platform application (port 7803) | Supabase (PostgreSQL) | Next.js (App Router), React 19 |
| tanstack-web | apps/tanstack-web | Replacement frontend for apps/web | Supabase (shared) | TanStack Start + Router + Query, Vite, React 19 |
| backend | apps/backend | Dedicated Rust API runtime (port 7820) | Supabase (shared) | Rust (Axum), Cloudflare Workers-ready |
| rewise | apps/rewise | AI-powered chatbot | Supabase (shared) | Next.js, AI SDK |
| nova | apps/nova | Prompt engineering platform | Supabase (shared) | Next.js, AI SDK |
| calendar | apps/calendar | Calendar & scheduling | Supabase (shared) | Next.js, React |
| finance | apps/finance | Finance management | Supabase (shared) | Next.js, React |
| tasks | apps/tasks | Task management (hierarchical) | Supabase (shared) | Next.js, React |
| meet | apps/meet | Meeting management | Supabase (shared) | Next.js, React |
| shortener | apps/shortener | URL shortener service | Supabase (shared) | Next.js |
| database | apps/database | Database migrations & schema | Supabase | SQL, TypeScript |
| discord | apps/discord | Discord bot utilities | None (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
- Shared Code: Common utilities, UI components, types shared across services
- Atomic Changes: Change types in one commit, update all services
- Simplified Tooling: Single build system (Turborepo + Bun)
- Easy Refactoring: Move code between services, extract packages
- 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
| From | To | Pattern | Protocol | Use Case |
|---|
| any app | Supabase | Shared DB | PostgreSQL + RLS | Read/write product data (primary path) |
| any app | Trigger.dev | Background job | task() trigger | Async/scheduled work |
| External | app | REST API | HTTP/JSON | Public API access (/api/v1/...) |
| tanstack-web | backend | REST | HTTP/JSON | Migrated read/write endpoints (Rust) |
| app (client) | app (server) | TanStack Query | HTTP/JSON | Client fetching via internal-api helpers |
| discord | web | Webhook | HTTP/JSON | Discord 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', { /* ... */ });
The repo’s root AGENTS.md carries the package-extraction decision matrix. As a
rough heuristic, extract when ≥3 HIGH signals:
| Signal | Extract Now (HIGH) |
|---|
| Reuse Breadth | Actively duplicated in ≥2 apps |
| Complexity | >150 LOC multi-module |
| Domain Ownership | Pure cross-domain utility |
| Testing Needs | Comprehensive 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):
-
Service-Specific Databases (when needed)
- Extract finance data to dedicated DB
- Communicate via events
- Maintain consistency with sagas
-
API Gateway Layer (if REST APIs grow)
- Single entry point for external clients
- Route to appropriate services
- Handle auth, rate limiting centrally
-
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.