No server/client split
There are no server components, no client components, no'use client' directives. Every component you write works on both the server and the client automatically.
In frameworks like Next.js, you constantly decide: “Is this a server component or a client component? Can I use hooks here? Do I need a directive?” In Vertz, you don’t think about it. Write your component with state, event handlers, and queries — the framework runs it on the server for the initial HTML and on the client for interactivity. Same code, both environments, zero configuration.
How it works
- Discovery — A lightweight execution of your app captures all
query()calls without rendering to HTML - Prefetch — The server awaits all discovered queries with per-query timeouts
- Render — A single render pass with pre-populated data produces the final HTML
- Hydration — The client picks up the server-rendered DOM and data without re-fetching
App entry
Yourapp.tsx exports the app component and metadata for SSR:
App(ordefault) — the app componenttheme— design tokens compiled to CSSstyles— global CSS strings injected into<head>getInjectedCSS— collects component-level CSS fromcss()calls
Client entry
The client entry hydrates the server-rendered HTML:mount() always targets #app — the framework controls the HTML in every context (SSR, dev server, templates), so there’s no selector to configure. It performs tolerant hydration — walking the existing server-rendered DOM and adopting it instead of replacing it. If hydration fails, it falls back to full client-side rendering.
Query pre-fetching
Queries declared withquery() are automatically pre-fetched during SSR:
Slow queries and streaming
Queries have an SSR timeout (default 300ms). If a query doesn’t resolve in time, the server sends the initial HTML with that query in its loading state — but it doesn’t give up. The server keeps waiting for the query to resolve and streams the result to the client as an inline<script> chunk once it’s ready.
The client listens for these streamed chunks and hydrates the query data without making a new HTTP request. No double-fetch, no wasted work.
You can override the timeout for a specific query if it’s expected to be slow:
Router SSR
The router detects SSR automatically. During server rendering, it reads the URL from the SSR context instead ofwindow.location:
Dev server
The Vertz dev server handles SSR automatically:- Compiles
.tsxfiles through the Vertz compiler - Server-side renders every page request
- Pre-fetches query data during SSR
- Injects scoped CSS into the response
- Hot-reloads with state preservation (HMR)
CSS during SSR
CSS is collected and injected during server rendering:- Theme CSS — compiled from your
defineTheme()tokens - Global styles — from the
stylesexport - Component CSS — from
css()andglobalCss()calls, collected viagetInjectedCSS()
AOT-compiled SSR
Vertz production builds use AOT (Ahead-of-Time) compiled SSR by default — components are pre-compiled into direct string concatenation functions at build time. No configuration needed:vertz build compiles the functions and vertz start loads them. Components that can’t be statically analyzed fall back to the DOM shim automatically.
How it works
The compiler analyzes each component and classifies it into a tier:| Tier | What it means | Example |
|---|---|---|
| Static | No dynamic data — pure string | <footer>© 2026</footer> |
| Data-driven | Uses props/data, but no reactivity | <h1>{title}</h1> |
| Conditional | Has ternaries, &&, or .map() | {isAdmin && <AdminPanel />} |
| Runtime fallback | Contains hooks, effects, or complex state — falls back to DOM shim | Components using useContext(), effect() |
query() are compiled with data access via the AOT context. The query data is pre-fetched using the same zero-discovery pipeline as the prefetch manifest, then passed to the AOT function:
Runtime holes
When the AOT compiler encounters a component it can’t compile (tier: runtime-fallback), it creates a hole — a placeholder that falls back to DOM shim rendering at request time. The AOT function calls into the hole, which runs the component through the standard SSR pipeline and returns HTML:Hydration markers
AOT output includes the same hydration comment markers as the DOM shim, ensuring the client-side hydration cursor can walk the server-rendered HTML identically regardless of which SSR path produced it:data-v-id="ComponentName"— interactive components that need client-side hydration<!--conditional-->...<!--/conditional-->— ternary and&&expressions<!--list-->...<!--/list-->—.map()rendered lists<!--child-->— reactive text content (start anchor)
Diagnostics
In development, the AOT pipeline provides diagnostic tools: Diagnostic endpoint —GET /__vertz_ssr_aot returns a JSON snapshot with per-component tier classification, AOT coverage percentage, and any divergences detected:
VERTZ_DEBUG=aot, the server dual-renders each page (AOT + DOM shim) and compares the output. Any mismatch is logged and recorded in the diagnostics snapshot. This catches hydration-breaking differences during development.
Classification logging — With VERTZ_DEBUG=aot, the build pipeline logs per-component tier classification, making it easy to see which components are AOT-compiled and which fall back to the DOM shim.
Server-side navigation
When navigating between pages on the client, Vertz can pre-fetch the next page’s data from the server:serverNav enabled:
- Client sends a pre-fetch request with
X-Vertz-Nav: 1header - Server runs discovery and prefetch for the target page
- Data streams back via Server-Sent Events
- Client receives data before rendering the new page
Production SSR handler
For production deployments,createSSRHandler() creates a web-standard (Request) => Promise<Response> handler:
Zero-discovery with a prefetch manifest
When you provide a prefetch manifest, the handler can skip the discovery pass entirely — queries are reconstructed from build-time metadata and prefetched without executing the component tree:Handler options
| Option | Description | Default |
|---|---|---|
module | The SSR module (server entry) | Required |
template | HTML template string | Required |
ssrTimeout | Per-query timeout in ms | 300 |
manifest | Prefetch manifest for zero-discovery SSR | — |
inlineCSS | Map of CSS URLs to content for inlining | — |
nonce | CSP nonce for inline scripts | — |
fallbackMetrics | Pre-computed font fallback metrics | — |
modulepreload | Paths to inject as modulepreload links | — |
routeChunkManifest | Per-route chunk manifest for targeted preloading | — |
cacheControl | Cache-Control header for HTML responses | — |
sessionResolver | Async function to resolve session from request | — |
aotManifest | AOT manifest for pre-compiled SSR rendering | — |
AOT is enabled by default
AOT rendering requires zero configuration.vertz build generates the AOT manifest automatically, and vertz start loads it at startup. Routes with AOT-compiled components are rendered via string-builder functions; routes without AOT entries fall back to ssrRenderSinglePass() transparently. The HTML output is identical either way — AOT is purely a performance optimization.
If the build can’t generate the AOT manifest (e.g., components that can’t be statically analyzed), the server falls back to DOM shim rendering for those routes. There’s nothing to configure — it just works.
If you’re writing a custom production server instead of using
vertz start, call
loadAotManifest() to load the manifest and pass it as the aotManifest option to
createSSRHandler(). See the custom server example below.Disabling AOT
To disable AOT rendering (e.g., for benchmarking the DOM shim path), omit theaotManifest option when creating a custom handler, or delete the generated aot-manifest.json from dist/server/.
Custom server with AOT
When writing a custom production server, load the manifest explicitly:loadAotManifest() returns null if the manifest files don’t exist, so it degrades gracefully.
How hydration works
When the client callsmount(), it finds server-rendered HTML already in the DOM. Instead of throwing it away and re-creating everything, hydration walks the existing DOM and adopts each node — attaching event handlers and reactive effects to what’s already there.
Cursor-based claiming
Hydration uses a global cursor that tracks position in the DOM tree. As your component tree executes on the client, each element or text node claims the corresponding SSR node at the cursor position:claimElement skips past non-matching elements and text nodes to find the expected tag further in the sibling list. This makes it tolerant of browser-injected nodes (extensions, whitespace). claimText is more conservative — it stops at element nodes without consuming them, preventing it from stealing an element that a subsequent claim expects.
Tolerant claiming
Claim functions are tolerant — if no matching node is found, they returnnull and the framework creates the element from scratch (CSR fallback). This means minor SSR/client mismatches don’t crash the page.
Claim functions are also transactional — a failed claim restores the cursor to its position before the attempt. This prevents a failed claim from corrupting the cursor and breaking all subsequent claims in the same tree.
Fallback behavior
If an unhandled exception occurs during hydration,mount() falls back to full client-side rendering — clearing the SSR HTML and re-creating the DOM from scratch. This is a last resort. In practice, tolerant claiming handles individual mismatches silently by creating missing nodes from scratch without interrupting the rest of the tree.
Debugging hydration
In development, hydration automatically logs warnings when claims fail or SSR nodes are left unclaimed after hydration finishes. These unclaimed-node warnings fromendHydration() are often the first signal that something is mismatched.
For production or browser-only debugging, set window.__VERTZ_HYDRATION_DEBUG__ = true before calling mount() to enable detailed cursor movement logging in the console. This shows exactly which nodes are claimed, skipped, or missed — useful for diagnosing why an element isn’t interactive after page load.