Architecture Overview
The three pillars that keep Mandu apps — and the agents writing them — from drifting.
On this page
Architecture Overview
Mandu is built around one belief: agents will write most of your code, and the framework has to stop them from breaking the house. Everything else — routing, rendering, bundling — is shaped to support that outcome.
Three pillars carry that weight. Each has its own deep dive; this page is a map.
The three pillars
1. Prerender — static by default
Every route in Mandu is statically prerendered at build time unless it explicitly opts out. No "getServerSideProps". No ambiguous "will it stream or hydrate?" Every page produces an HTML file, and that HTML is what the user sees first.
Why this matters for agents: an agent generating a page doesn't have to decide between five rendering modes. There is one default, and it is the fastest one.
// app/about/page.tsx — prerendered, zero config
export default function About() {
return <article>About Mandu.</article>;
}
Opt into dynamic rendering explicitly:
// app/dashboard/page.tsx
export const dynamic = "force-dynamic";
export default async function Dashboard() {
const user = await getCurrentUser();
return <Greeting user={user} />;
}
Deep dive: Prerender.
2. Island — interactivity is opt-in
Client-side JavaScript ships only from files that end in .island.tsx.
Everything else — pages, layouts, shared components — is server-rendered HTML
with zero hydration cost.
// src/client/widgets/counter/Counter.island.tsx
"use client";
import { useState } from "react";
export function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}
// app/page.tsx — server-rendered, imports an island
import { Counter } from "@/client/widgets/counter/Counter.island";
export default function Home() {
return (
<main>
<h1>Welcome</h1>
<Counter />
</main>
);
}
The .island.tsx suffix is the only signal the bundler needs to decide
what ships to the browser. No manifest file, no "use client" scattered across
the tree — the filename is the contract.
Deep dive: Island.
3. Guard — invariants the agent can't break
Guard is a static analyzer that runs on every build (and via bun run guard
on demand). It enforces rules the framework cares about, such as:
src/server/**cannot be imported fromsrc/client/**orapp/**.page.tsxfiles cannot declare"use client"..island.tsxfiles must declare"use client".- API route files (
app/**/route.ts) must export a defaultMandu.filling()handler.
When Guard fails, the build fails. The error message points to the exact file and line and explains which invariant was violated. Agents read that message and self-correct.
Guard FAIL src/client/widgets/chart/Chart.island.tsx:3
Invariant: no-server-in-client
Imported src/server/infra/db.ts from a client island.
Fix: move the data fetch into an API route or loader.
Deep dive: Guard.
How Mandu differs from Next.js and Vite
| Concern | Next.js | Vite (plain) | Mandu |
|---|---|---|---|
| Runtime | Node.js | Node.js | Bun only |
| Default render mode | Hybrid (RSC + CSR) | CSR | Prerender |
| Client boundary signal | "use client" everywhere |
Ad hoc | .island.tsx filename |
| Architecture enforcement | Linter plugin (optional) | None | Guard (build-time, mandatory) |
| Router | app/ directory |
User-chosen | app/**/page.tsx only |
| Agent integration | External | External | First-party MCP server |
| Config required | Often (next.config.js) |
Yes (vite.config.ts) |
Optional (mandu.config.ts) |
| Build output | .next/ (mixed) |
dist/ (static) |
dist/ (static + minimal server) |
The table simplifies — every framework can be bent into other shapes — but the defaults are what matter when an agent is generating code without human review.
How the pillars compose
A typical request flow in production:
HTTP request
│
▼
┌──────────────┐ ┌──────────────┐
│ Prerender │──► │ Static HTML │ ← 99% of traffic stops here
└──────┬───────┘ └──────────────┘
│ (dynamic routes only)
▼
┌──────────────┐ ┌──────────────┐
│ Server │──► │ Streamed HTML│
└──────┬───────┘ └──────────────┘
│
▼
┌──────────────┐ ┌──────────────┐
│ Island │──► │ Hydrated JS │ ← only where marked
└──────────────┘ └──────────────┘
Guard runs at build time across every arrow above.
- Prerender handles the common case (marketing, docs, dashboards with client-side data).
- Islands handle interactivity without pulling the whole page into the client bundle.
- Guard runs across every boundary at build time so none of the above degrades silently when 10 agents commit to the repo in one day.
When to read which page
- You're laying out a new app → Routing.
- You're deciding where a file belongs → Layers.
- You're tuning what the browser downloads → Island.
- You're debugging a failed build → Guard.
Do not do this
- Do not try to make a page "partially interactive" by adding
"use client"to it. The right tool is an island.- Do not disable Guard to unblock a build. Fix the violation; that's the signal working as intended.
- Do not mirror Next.js patterns blindly.
getServerSideProps,middleware.tsat the route level, and_app.tsxdo not exist in Mandu.
🤖 Agent Prompt
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/overview to my project.
Summary of the page:
Mandu rests on three pillars — Prerender, Island, Guard. Prerender is the default render mode, Island is the opt-in client boundary, Guard is the machine-checkable invariant layer.
Required invariants — must hold after your changes:
- Every route is prerendered unless it explicitly opts into dynamic rendering
- Interactivity is opt-in via .island.tsx files; nothing ships to the client by default
- Guard rules are enforced at build time and fail the build on violation
- src/server/** never crosses the client boundary
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
For Agents
Mandu rests on three pillars — Prerender, Island, Guard. Prerender is the default render mode, Island is the opt-in client boundary, Guard is the machine-checkable invariant layer.
- Every route is prerendered unless it explicitly opts into dynamic rendering
- Interactivity is opt-in via .island.tsx files; nothing ships to the client by default
- Guard rules are enforced at build time and fail the build on violation
- src/server/** never crosses the client boundary