Read this as patterns, not literal architecture. Tuturuuu is not built on a
Kafka-style event bus with an immutable event log, replayable streams, or
dead-letter queues. It runs conventional Next.js apps that enqueue
background tasks on Trigger.dev (a managed jobs platform) and persist state
in a shared Supabase Postgres database. Sections below are labelled
Concept (general EDA theory) or In Tuturuuu (verified against real
code). Treat any capability not shown in real code as illustrative.
Active migration.
apps/web (Next.js, port 7803) is being replaced by
apps/tanstack-web (TanStack Start) plus apps/backend (Rust, port 7820).
Background-job ownership may shift during this transition — some asynchronous
work that runs as a Trigger.dev task today may move into the Rust backend. See
TanStack + Rust Migration.
Nothing in this page is a permanent architectural commitment.Overview
Concept. In a fully event-driven system, services communicate asynchronously through events published to a central broker. Services are producers (publishing events) and consumers (subscribing to and processing events), with the broker handling delivery, persistence, and reliability. In Tuturuuu. There is no central event bus. Instead:- A request handler (an
apps/webAPI route or a cron entrypoint) does its synchronous work and then enqueues a Trigger.dev task for follow-up work. - Trigger.dev runs that task on its managed infrastructure, with retries, concurrency control, and queues.
- An “orchestrator” task can fan out by triggering one child task per
workspace via
task.trigger(...).
packages/trigger/src — for example
google-calendar-sync.ts, google-calendar-full-sync.ts,
unified-schedule.ts, and schedule-tasks.ts. They are defined with the
Trigger.dev v4 task() API (@trigger.dev/sdk ^4.4.5), imported from the
@trigger.dev/sdk/v3 entrypoint.
Advantages of Event-Driven Design
1. Decoupling work from the request path
Concept. A producer publishes an event and is done; it does not know which consumers react. This is the loosest possible coupling and lets you add or remove downstream behavior without touching the producer. In Tuturuuu. We get a pragmatic version of this: an API route finishes its synchronous work, then enqueues a task instead of doing the slow work inline. The route does not care how the task is implemented, and the task can evolve independently.- Keep user-facing requests fast by moving slow work off the request path.
- Change a task’s internals without changing its callers.
- Add new background behavior by adding a new task, not by editing the producer.
Tuturuuu does not broadcast a single event to many anonymous subscribers.
Callers explicitly enqueue named tasks. There is no event-name pub/sub fan-in
layer between producer and consumer.
2. Resilience through retries
Concept. A durable broker keeps events until a consumer processes them, so a consumer outage does not lose work — it just delays it. In Tuturuuu. Trigger.dev persists each enqueued task run and retries failed runs on its managed infrastructure, so a transient failure (an expired Google token, a flaky network call) does not silently drop the work. Tasks are written to return a structured{ success, error } result and to log failures rather
than crash the caller.
- No cascading failures: the API route that enqueued the task is unaffected if the task fails.
- Automatic retries: Trigger.dev re-runs failed task runs.
- Durability: enqueued runs survive a worker restart.
Concept, not implemented in Tuturuuu: an immutable event log you can
re-read from the beginning, plus dead-letter queues for permanently failed
events. Trigger.dev retries and surfaces failed runs in its dashboard; our
tasks additionally swallow errors into a structured result so an orchestrator
can continue with the next workspace. There is no
*.dlq wildcard trigger or
DLQ topic in this repo.3. Scalability through concurrency control
Concept. Multiple consumer instances can process the same stream in parallel, and the broker levels load by buffering spikes. In Tuturuuu. Trigger.dev runs many task runs concurrently, and we cap that concurrency per task with aqueue so a burst of work cannot overwhelm shared
resources (Postgres, the Google Calendar API). An orchestrator fans out one
child run per workspace and gives each its own concurrencyKey.
- Bounded throughput:
queue.concurrencyLimit(e.g.10forscheduleTask,5forunifiedScheduleTask) protects shared dependencies. - Per-key isolation:
concurrencyKeykeeps each workspace’s runs from stepping on each other. - Spike absorption: Trigger.dev queues work it cannot run immediately.
4. Temporal decoupling
Concept. With a persistent, replayable event log, a brand-new consumer can read the entire history of events to bootstrap itself — useful for rebuilding read models, training models, or running new analyses on past data. If you genuinely need history-as-a-stream in the future, you would build it explicitly (e.g. an append-only Postgres table plus a job that reads it) — it is not a free property of the current stack.Drawbacks of Event-Driven Design
1. Harder to reason about and debug
Concept. Asynchronous, choreographed work has no single linear call stack, so tracing “why did this fail?” spans multiple independent runs. In Tuturuuu. The same trade-off applies the moment work moves off the request path. A failing calendar sync does not show up in the API route’s stack trace; you find it in the Trigger.dev run logs. We lean on:- The Trigger.dev dashboard for per-run logs, retries, and status.
- Structured
console.log/console.errorinside tasks, keyed byws_id, so runs are correlatable. - Returning a structured
{ ws_id, success, error }result from each task so an orchestrator can report per-workspace outcomes.
There is no
@tuturuuu/logging or @tuturuuu/observability package and no
cross-service distributed-tracing system wired into these tasks today.
Inside apps/web runtime code, use the internal log-drain logger rather than
raw console.*; the Trigger.dev tasks in packages/trigger log via
console.* to the Trigger.dev run logs.2. Eventual consistency
Concept. Because processing is asynchronous, there is a delay between when work is enqueued and when its effects are visible. The system is “eventually consistent,” and the UX must accommodate that. In Tuturuuu. A calendar sync or auto-schedule run completes some time after it is enqueued, so freshly triggered changes are not instantly reflected. Handle this the way the rest of the platform does:- Optimistic UI and loading states via TanStack Query while background work settles.
- Design flows so the user is not blocked waiting on a background task.
- Optimistic UI updates with background reconciliation.
- Clear loading/processing states.
- Read-your-own-writes via a direct query path when a value must be immediate.
3. Payload/schema governance
Concept. Event payloads become a contract between producer and consumer, so schema evolution must be backward compatible or versioned. In Tuturuuu. Each task’spayload type is its contract. Because callers and
tasks live in the same monorepo and TypeScript checks both sides, breaking a
payload shape surfaces at type-check time rather than silently at runtime. Keep
payload changes additive (new optional fields) and validate untrusted input with
zod where a payload crosses a trust boundary.
- Additive payload changes only; avoid removing or retyping existing fields.
- Let the monorepo’s shared types and
bun checkcatch mismatches. - Validate external/untrusted payloads with
zod.
4. Operational dependency on the jobs platform
Concept. A central broker is critical infrastructure that must be operated, tuned, and monitored. In Tuturuuu. Using managed Trigger.dev removes most of that operational burden — there is no broker to run. What remains is application-level care:- Make task work idempotent where possible, since runs can retry.
- Set sane
queue.concurrencyLimitvalues to protect Postgres and external APIs. - Watch the Trigger.dev dashboard for failure rates and run latency.
- Keep secrets (e.g.
INTERNAL_TRIGGER_SECRET_KEY,GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET) in environment variables, never in code.
- Managed platform (Trigger.dev) instead of self-hosting a broker.
- Idempotent, retry-safe task bodies.
- Concurrency limits and per-workspace
concurrencyKeys.
How Background Work Actually Runs in Tuturuuu
Defining a task (Trigger.dev v4)
Tasks are defined withtask() and exported from packages/trigger/src. The SDK
is @trigger.dev/sdk ^4.4.5; tasks import task from @trigger.dev/sdk/v3.
Enqueuing and fanning out
Other code triggers a task by calling.trigger(payload, options?) on the
exported task. An orchestrator triggers one child run per workspace, isolating
each with a concurrencyKey (see googleCalendarFullSyncOrchestrator above).
Scheduling
When to Reach for Asynchronous (Event-Driven) Work
Enqueue a background task when:- The work is slow and should not block the HTTP response (calendar sync, bulk processing, external-API fan-out).
- Eventual consistency is acceptable for the use case.
- Retries and bounded concurrency add real value.
- The work fans out across many workspaces or items.
- The caller needs the result immediately to return a response.
- The user is waiting in real time and cannot tolerate a delay.
- It is a simple CRUD operation with no slow side effects.
- The added indirection would only make debugging harder.
Related Documentation
- Architectural Decisions — system-design trade-offs
- Extensibility, Resilience & Scalability — these benefits in detail
- Trigger.dev Package — the real task implementations and conventions
- TanStack + Rust Migration — where background work is heading
- Development Tools — local development and verification workflow