โš™๏ธ Deep Dive

The Product & The Loop โ€” Under the Hood

What the no-API architecture actually costs, what else was on the table, and where a modular monolith breaks.

What a page view actually costs

Wera's reads look free โ€” a function call, no HTTP hop to an API. But the database driver is the Neon HTTP driver: every SQL statement is its own HTTPS round trip. That single fact shapes the performance profile of the whole codebase.

OperationCostWhy
Render the pipeline pageO(1) queriesOne aggregate query returns applications with needs-flags โ€” the SPEC forbids per-row lookups (no N+1)
Each SQL statement~1 HTTPS round tripHTTP driver: no pooled connection, no pipelining โ€” latency โ‰ˆ statements ร— RTT
Server action writeO(k) statementsk = side effects (import = company + posting + application + event + task + reminder โ‰ˆ 6)
Discovery ingest of n itemsO(n) ยท ~5 stmts/itemSequential upserts per item; a 500-item run โ‰ˆ 2,500 round trips โ€” hence the volume caps
Monorepo typecheck/testO(changed packages)Turborepo caches task outputs per package hash; unchanged packages replay from cache

Back-of-envelope: at ~15 ms RTT to Neon, the six-statement manual import spends ~90 ms in the database and 2โ€“10 seconds in the AI call โ€” the model dominates by two orders of magnitude, which is why the code optimizes for correctness of DB writes rather than batching them. The place where round trips do dominate is bulk ingestion, and that's exactly where the code adds caps (200 manual / 500 scheduled) and progress flushes every 50 items.

Scaling note: at 100ร— the data (a real multi-user SaaS), the first things to move are (1) ingestion โ†’ batched inserts or a queue, (2) the list query โ†’ pagination (it currently returns every application the user has), and (3) the HTTP driver โ†’ pooled connections via Neon's WebSocket driver on the background workers.

The decision: a modular monolith with no API layer

Wera ships one deployable app whose pages call the database in-process, plus internal packages as boundaries. Here's the design space it navigated:

Chosen
Modular monolith + server actions

Types flow unbroken from SQL schema to JSX โ€” a renamed column is a compile error in the UI.

No API versioning, serialization, or auth-token plumbing between "frontend" and "backend."

Package boundaries preserve testability and future extraction points.

No independent scaling or fault isolation; everything shares one deploy and one failure domain.

Alternative
SPA + REST/GraphQL API

Clean contract for future mobile apps or third-party clients.

Frontend and backend teams can move independently.

Two codebases' worth of types, auth, and error handling for a one-person product.

Every feature costs an endpoint + client hook + loading state โ€” the tax Wera explicitly avoided.

Alternative
Microservices (discovery, resume, tracker as services)

Independent scaling; a scraper crash can't take down the app.

Distributed-systems tax (service discovery, network failures between features, observability) at a scale of one user โ€” pure overhead.

The judgment: the SPA+API split wins the day Wera needs a mobile client or opens an API to other tools; microservices win roughly never at this product's ceiling. Until then, the monolith's compile-time type unity is worth more than theoretical isolation. Note the hedge the team kept: because packages already isolate discovery, resume, and AI, extracting a service later is a re-wiring job, not a rewrite.

The same risk-management shape appears in the process decision: full Phase-0 foundations (Trigger.dev, R2, CI from day one) were flagged by a reviewer as schedule risk; the founder kept them with a tripwire โ€” "If Phase 0 exceeds one week, revisit." Decisions with named reversal conditions beat both stubbornness and flip-flopping.

Where a monolith bites

The unhappy paths of this architecture are mostly shared fate and configuration seams:

โš ๏ธ
The silent engine swap

resolveAiClientFromEnv picks the deterministic heuristic client unless AI_PROVIDER=openai or NODE_ENV=production. A staging box with a mis-set NODE_ENV silently serves canned heuristic analysis that looks plausible. This class of bug โ€” an environment seam choosing an implementation โ€” is why the TODOS log records landing "Real LLM provider env seam" as an explicit fix: the seam must be loud, not clever.

  • Shared failure domain: a runaway discovery ingest and the scoreboard render share the same Postgres and the same Vercel limits. The mitigations are budget-shaped: ingest caps, fetch timeouts (10 s connectors, 5 s job-URL), and AI spend ceilings โ€” every external interaction has a numeric leash.
  • Serverless duration cliffs: a server action that does network work (fetch โ†’ AI โ†’ 6 DB writes) runs against a hard platform timeout. That's why anything unbounded (full public-list sync, PDF compile) is pushed to Trigger.dev tasks with their own maxDuration declarations (60 s compile, 300 s discovery).
  • Cache staleness as a correctness bug: with no API layer, freshness is controlled by revalidatePath() calls in each action. Forget one and users see stale data with no error anywhere โ€” the failure is invisible. Notice how each action revalidates every surface it touches (/, /pipeline, /applications/[id]).
  • Concurrency floor: most read-then-write sequences (status updates, budget checks) are not transactions. At one user, interleavings are rare and the code chooses idempotent writes over locks; Module 6's deep dive shows where that line actually holds and where it's luck.

Make it yours

๐Ÿ›  Exercise 1
Trace a write end-to-end

Open apps/web/src/app/actions.ts and pick markAppliedAction. Without running anything, write down every file the call touches from button click to pixels updating, and every SQL statement it issues. Then check yourself against packages/db/src/applications.ts:695.

Hint

Count the statements inside markApplied: one select, then (if changing) one update + one event insert + one task update + one task upsert, then the weekly count. What makes calling it twice safe?

๐Ÿ›  Exercise 2
Find the missing revalidation

Suppose you add a "snooze reminder" action that updates a reminder row but forgets revalidatePath("/plan"). Describe exactly what the user experiences, and why no log line anywhere will mention it. Then propose a convention (naming, lint rule, or helper) that makes this bug class hard to write.

Hint

The write succeeds; the page cache doesn't know. Consider a helper like revalidateApplicationSurfaces(applicationId) that owns the list of paths.

๐Ÿ›  Exercise 3
Argue the other side

Write a one-page memo arguing Wera should have been an SPA + REST API from day one. Steelman it: name two concrete features on the roadmap (SPEC ยง16) that get cheaper with an API. Then write the rebuttal the spec would give.

Hint

The browser extension (V2) and Gmail/Calendar integrations (V1.5) both want a stable programmatic surface. The rebuttal is about when that surface should be paid for.

๐Ÿ“–
Data Security & Server Actions โ€” Next.js docs

The authoritative treatment of what server actions expose and how to keep them safe โ€” the exact model Wera's ~30 actions rely on.

๐Ÿ“–
MonolithFirst โ€” Martin Fowler

The classic argument for exactly the architecture bet Wera made, including when to reverse it.

๐Ÿ“–
Neon serverless driver โ€” official docs

Read "How it works" to understand the per-statement HTTP round trip that shapes this codebase's data-access patterns.