LangENKO

Architecture Overview

The three pillars that keep Mandu apps — and the agents writing them — from drifting.

since v0.22
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 from src/client/** or app/**.
  • page.tsx files cannot declare "use client".
  • .island.tsx files must declare "use client".
  • API route files (app/**/route.ts) must export a default Mandu.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.ts at the route level, and _app.tsx do not exist in Mandu.

🤖 Agent Prompt

🤖 Agent Prompt — Architecture Overview
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

AI hint

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.

Invariants
  • 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
Guard scope
architecture