Skip to main content
Security is a foundational architectural concern in Tuturuuu, embedded at every level of the system design. This document explains the core architectural patterns that provide comprehensive security coverage.
This document focuses on architecture-level security patterns. For implementation details, see:
What is real vs. illustrative. Tuturuuu’s web tier (apps/web) is a conventional Next.js App Router app, not a network of independently keyed microservices. Several patterns below (a single API gateway, service-to-service token exchange, an immutable event-sourced audit log) are presented as conceptual / target patterns to explain the reasoning — they are explicitly labeled where they are not implemented as written. The concrete code examples (the withApiAuth wrapper, Supabase Row-Level Security, the Rust backend’s Supabase auth) reflect the real codebase.The platform is also mid-migration: apps/web (Next.js, port 7803) is being replaced by apps/tanstack-web (TanStack Start) plus apps/backend (Rust, port 7820). See TanStack Start And Rust Migration for how security responsibilities move into the Rust backend.

1. Consistent Security Enforcement at the API Boundary

Architectural Choice

Rather than a literal standalone API gateway, Tuturuuu enforces security consistently at the API route boundary through a shared higher-order wrapper. Every public SDK route under apps/web/src/app/api/v1/* is wrapped with withApiAuth, which centralizes the security logic (API-key authentication, rate limiting, permission checks) in one tested implementation instead of re-deriving it per route.
Conceptual vs. real. “API gateway” describes the pattern — a single, uniform enforcement point — not a literal gateway process in front of microservices. In apps/web there is no separate gateway tier; security is enforced per route. The uniformity comes from sharing the withApiAuth wrapper (apps/web/src/lib/api-middleware.ts), Next.js middleware.ts, and Vercel’s edge. In the TanStack + Rust migration, the Rust backend (apps/backend) becomes the primary boundary that validates Supabase sessions.

Impact and Justification

By funneling authentication and authorization through one shared wrapper, we create a single security control plane in code: policies are defined once and applied consistently before any business logic runs. Routes do not re-implement token parsing, key validation, or rate-limiting individually, which removes the drift and duplication that cause inconsistent security postures. Conceptual enforcement flow (illustrative, not a literal gateway process):
Real example from Tuturuuu’s SDK API (apps/web/src/app/api/v1/storage/list/route.ts):
Note that rateLimit is optional: by default withApiAuth leaves GET/HEAD reads open and applies a per-key mutation limit (about 100 requests/minute), with workspace-specific overrides read from workspace_secrets. Pass a rateLimit object only to tighten or relax those defaults.

Clarifying Additions

Shared enforcement ensures uniform security behavior across routes. Instead of each route implementing authentication differently, withApiAuth provides a single, tested implementation every endpoint benefits from. Policies update consistently without touching each route. When a new requirement emerges (e.g., a stricter default rate limit), it is implemented once in the wrapper and immediately applies everywhere it is used. This strengthens protection by eliminating inconsistent security implementations. Human error is reduced because developers do not re-implement the same security logic across many routes.

2. Zero Trust Principles Applied at the Architectural Level

Architectural Choice

The architecture implements Zero Trust security principles, where no service or request is inherently trusted based on its network location or source. Every interaction, whether from external clients or between internal services, must be explicitly authenticated and authorized.

Impact and Justification

Traditional “castle-and-moat” security assumes that anything inside the network perimeter is trusted. This is a dangerous assumption because once an attacker breaches the perimeter, they have unrestricted access to internal systems. Zero Trust architecture eliminates implicit trust. Every request is re-validated for identity and permissions at the boundary it crosses, and the database independently enforces access rules regardless of which app issued the query.
No inter-service token system exists. Tuturuuu does not implement service-to-service token exchange (generateServiceToken, X-Service-Id headers, per-service token verification). The apps share a single Supabase Postgres database, and Zero Trust is realized through (a) request authentication at each API boundary and (b) Row-Level Security in the database — not a mesh of mutually authenticating services. The example below shows how the apps actually establish identity, not a fictional token broker.
How identity is established in practice. Every request that reaches data must carry a verifiable identity, and that identity is re-checked at the boundary:
  • User requests carry a Supabase session (cookie or Authorization: Bearer access token). The Rust backend validates these in apps/backend/src/supabase_auth.rs, which extracts the access token from the sb-*-auth-token cookie (including base64-prefixed, chunked cookies) or a non-app Bearer token, then calls the Supabase Auth /user endpoint with the service-role key to resolve the user.
  • SDK / machine requests carry a workspace-scoped API key, authenticated and rate-limited by withApiAuth in apps/web before any handler runs.
There is no per-service shared secret to leak: trust derives from a Supabase-issued token (validated against Supabase) or a workspace API key (validated against the database), and the database enforces the final word via RLS. Database-level Zero Trust (Row-Level Security):

Clarifying Additions

Identity is established from verifiable tokens, not network location. A request is trusted because it carries a valid Supabase session or workspace API key that is re-validated at the boundary — not because it originated inside the deployment. The database has the final say. Even if an API layer were bypassed or compromised, RLS policies independently enforce that a user can only read or write rows for workspaces they belong to. Application bugs cannot silently defeat data-access rules. This reduces risks associated with implicit trust. Because authorization is enforced both at the boundary and in the database, a single compromised layer does not grant unrestricted data access.

3. Security Boundaries Through RLS and Permission Scoping

Architectural Choice

Isolation between domains (finance, AI chat, drive, tasks) is enforced primarily by per-table Row-Level Security policies and scoped permissions, not by giving each app its own Supabase project or key. The apps share one Supabase database; the boundary lives in the policies and in the permission set each API key or user role is granted.
Per-app anon keys are not how this works. Earlier revisions of this page implied distinct WEB_ANON_KEY, FINANCE_ANON_KEY, and REWISE_ANON_KEY values backing separate “services”. That is not accurate — Tuturuuu’s apps share a single Supabase project and the same anon/service-role keys. Domain isolation comes from RLS policies and permission scoping on shared tables, described below.

Impact and Justification

In a single shared database, a vulnerability that bypasses one route must still defeat the per-table RLS policy to reach data. By scoping each domain’s tables with dedicated policies and permission requirements, we contain the blast radius: a bug in a drive route cannot read finance rows, because the finance policy requires finance-specific roles and the request’s identity does not satisfy them. Concretely, SDK routes declare the permissions they need (permissions: ['manage_drive']), and the database enforces an orthogonal check at query time. An attacker who somehow reaches a query path is still bounded by both the granted permission scope and the row-level policy. Data isolation through RLS policy design:

Clarifying Additions

A bug in one domain’s routes does not expose another domain’s data. RLS policies and permission scoping bound each domain independently, so reaching a finance table requires finance-grade identity even from an unrelated route. Policies act as an independent barrier alongside application checks. Defense in depth is achieved because the database re-validates access regardless of what the application layer believed. This scoping limits the damage in security incidents. Incident response is more manageable because the data an attacker could reach is bounded by the policies and permissions that gate each table.

4. Defense-in-Depth via Layered Architecture

Architectural Choice

The architecture implements multiple layers of security controls, from the network edge through the API Gateway, into the application services, down to the database layer. Each layer provides independent security validation.

Impact and Justification

Relying on a single security layer is fragile. If that layer is bypassed or fails, the entire system is exposed. The defense-in-depth approach ensures that even if an attacker successfully bypasses one security control, they encounter additional independent security barriers at deeper layers. For example, even if an attacker bypassed the API boundary’s authentication (highly unlikely but theoretically possible), they would still encounter:
  1. Route-level authorization checks (declared permissions) in the application code
  2. Database Row-Level Security policies that enforce data access rules
  3. Workspace scoping that bounds every query to the caller’s workspace
In Tuturuuu - Multiple Security Layers: Layer 1: Edge / Boundary Security
Browser framing boundary. Every Next.js app that uses createTuturuuuNextConfig receives Content-Security-Policy: frame-ancestors 'none' and X-Frame-Options: DENY on its responses. This prevents an attacker-controlled page from rendering Tuturuuu interfaces inside an iframe for UI redressing. App-specific headers() entries are composed after the shared security rule, so route-specific cache and indexing headers remain intact. The only shared exception is the CMS WebGL asset route /api/v1/workspaces/:wsId/external-projects/assets/:assetId/webgl/*. The CMS player intentionally renders that document in a sandboxed iframe without allow-same-origin; the asset route keeps its own restrictive CSP sandbox policy. New framing exceptions must be path-specific, justified by a documented product requirement, and preserve an equivalent sandbox boundary. Layer 2: Application-Level Security
Layer 3: Database-Level Security
Layer 4: Data Encryption

Clarifying Additions

Multiple layers ensure no single point of failure exposes sensitive logic. An attacker must defeat multiple independent security controls to compromise the system, making successful attacks exponentially harder. Higher layers validate and protect the domain before requests reach it. The most critical business logic (the domain core) is protected by multiple security barriers, ensuring it only processes validated, authorized requests. The system becomes safer by distributing security responsibility across layers. Security is not the responsibility of a single component but is woven throughout the architecture, creating a more resilient security posture.

5. Auditability and Background Security Workflows

Architectural Choice

Security-relevant actions should be traceable, and heavier security workflows (anomaly scans, notifications, periodic reviews) should run as durable background jobs. Tuturuuu uses Trigger.dev v4 task() for background work and native server console logs plus retained platform observability data for server-side diagnostics.
Aspirational, not implemented as written. A comprehensive, cryptographically signed, immutable, event-sourced audit log is a target pattern, not a shipped subsystem. Tuturuuu does not have an event store or event-sourcing layer, and there is no global audit_events table that captures every state change. Treat the audit-log code below as an illustration of the goal. What exists today: Supabase keeps row history where tables are designed for it, retained platform observability data records selected server-side events, Trigger.dev runs durable background tasks, and runtime diagnostics use the native console method matching severity.

Impact and Justification

Security is not just about prevention; it’s also about detection and accountability. A durable, append-only audit trail would provide:
  1. Forensic Analysis: After an incident, history lets investigators reconstruct what happened, when, and by whom
  2. Compliance: Regulations (GDPR, SOX, HIPAA) require audit trails of data access and modifications
  3. Anomaly Detection: Patterns can be analyzed to detect suspicious behavior (e.g., unusual access patterns, privilege escalation)
  4. Non-Repudiation: Append-only, tamper-evident records provide strong proof of actions
Target audit-write pattern (aspirational — illustrates intent):
Audit Log Query Interface (target shape):
Background security workflow (real Trigger.dev v4 task() API): Trigger.dev v4 uses the task() builder (@trigger.dev/sdk ^4.4.5). The v2 API (client.defineJob, eventTrigger, io.*) does not exist in this repo. A security-scan task looks like the calendar/schedule tasks under packages/trigger/src:
Trigger it from server code with securityScanTask.trigger({ userId }) (or batchTrigger for many). See the Event-Driven Architecture page for how Tuturuuu actually uses Trigger.dev v4 and native server console logs.

Clarifying Additions

Durable background tasks keep heavy security work off the request path. Anomaly scans and notifications run as Trigger.dev v4 tasks rather than blocking the user’s request. An append-only audit trail would support accountability and forensics. This is the target; today, rely on retained platform observability data, native server console logs, and table-level history for traceability. Compliance benefits from consistent logging. Use the native console method matching severity and retain structured metadata where platform observability wrappers are wired.

Security Architecture Summary

Security Best Practices in Tuturuuu

1. Never Trust, Always Verify

  • Every request validated at multiple layers
  • No assumptions about request origin or authenticity
  • All user input sanitized and validated

2. Principle of Least Privilege

  • Services granted minimum permissions needed
  • Users granted minimum roles required
  • API keys scoped to specific operations

3. Fail Securely

  • Default deny for authorization decisions
  • Explicit permission grants required
  • Errors don’t leak sensitive information

4. Defense in Depth

  • Multiple layers of security controls
  • Redundant security checks
  • No single point of failure

5. Audit Everything

  • All security-relevant events logged
  • Immutable audit trail
  • Regular security audits and reviews

Security Under the TanStack + Rust Migration

apps/web (Next.js, port 7803) is being replaced by apps/tanstack-web (TanStack Start) plus apps/backend (Rust, port 7820). Several security responsibilities move accordingly:
  • Boundary authentication shifts into the Rust backend (apps/backend/src/supabase_auth.rs), which validates Supabase access tokens from cookies or Bearer headers against Supabase Auth using the service-role key.
  • TanStack web must not call /trpc or raw app APIs directly (a guard, check-tanstack-api-access, enforces this); product data flows through packages/internal-api helpers and REST /api/v1 routes.
  • RLS remains the durable backstop across both stacks because both share the same Supabase project.
See TanStack Start And Rust Migration for the full migration contract.