# Tavo.js
> Tavo.js is a TypeScript-first framework ecosystem for CSR, SSR, routing, data, MVC components, stores, production tooling, and an optional accessible UI system.
Important agent guidance:
- Tavo.js is not React. Do not introduce React hooks, React context, or React rendering assumptions.
- Keep route concerns in page modules, reusable UI in components, behavior in TavoController classes, and reactive state in models or stores.
- Page modules may export `pending` for active client-route loading UI and `error` for a contextual page-loader failure; import `PagePendingProps` and `PageErrorProps` from `@tavojs/core/router`.
- Use `tavo agent-context --json` for bounded project context and `tavo verify --no-project-scripts --json` when verification must not invoke project scripts.
- Use @tavojs/mcp over local stdio for versioned Tavo.js knowledge and optional read-only project inspection; do not assume a public hosted endpoint is available.
- Use the generated Markdown and JSON exports for machine-readable documentation.
- Treat server/client boundaries, consent, accessibility, and SSR determinism as implementation requirements.
## Start
- [Getting Started](https://tavojs.dev/docs/getting-started.md): Build a representative application from setup through deployment.
- [Components, controllers, and stores](https://tavojs.dev/docs/getting-started/components-controllers-and-stores.md): Learn Tavo.js ownership and state conventions.
- [Server and client execution](https://tavojs.dev/docs/getting-started/server-and-client-execution.md): Keep browser and server work in the correct runtime.
## Framework
- [Pages and layouts](https://tavojs.dev/docs/core/pages-and-layouts.md): File routes, layouts, and typed page modules.
- [MVC components](https://tavojs.dev/docs/core/mvc.md): Components, models, controllers, and lifecycle.
- [Data loading and middleware](https://tavojs.dev/docs/core/data-and-middleware.md): Loaders, middleware, caching, and request data.
- [SSR and hydration](https://tavojs.dev/docs/core/ssr-and-hydration.md): SSR, hydration, and deterministic rendering.
- [Core API reference](https://tavojs.dev/docs/core/api.md): Search the published Framework API by responsibility and package boundary.
## UI
- [Tavo.js UI documentation](https://tavojs.dev/docs/ui.md): Tavo.js UI overview and installation path.
- [Accessibility](https://tavojs.dev/docs/ui/accessibility.md): Accessibility behavior and requirements.
- [Composition and imports](https://tavojs.dev/docs/ui/composition-and-imports.md): Supported composition and package imports.
- [Tavo.js UI component index](https://tavojs.dev/docs/ui/components/index.md): All public components with usage, props, examples, and accessibility guidance.
## Agent interfaces
- [Use Tavo.js with AI coding agents](https://tavojs.dev/docs/mcp.md): Connect MCP-compatible AI agents to Tavo.js documentation and optional read-only project inspection.
- [Complete LLM context](https://tavojs.dev/llms-full.txt): Combined authored documentation and component reference.
- [Versioned JSON manifest](https://tavojs.dev/.well-known/tavo-docs-v1.json): Structured documentation, APIs, components, tokens, CLI help, versions, and content hash.
## Optional
- [Automation and JSON protocol](https://tavojs.dev/docs/cli/automation.md): Bounded machine workflows, dry runs, receipts, and verification.
- [UI Core overview](https://tavojs.dev/docs/ui-core.md): Platform-neutral tokens and accessibility primitives.
# Complete Tavo.js documentation
# Getting Started
> Learn the Tavo.js application model from installation through production, one focused topic at a time.
Canonical page: https://tavojs.dev/docs/getting-started
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: quickstart
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- HTML, CSS, TypeScript, and TSX fundamentals.
- Node.js 20.19+ or 22.12+, a terminal, an editor, and a modern browser.
## Outcomes
- Choose a task-first guide or follow the complete beginner learning path.
- Understand how Tavo.js separates routes, request data, local behavior, server work, and build output.
## Before you begin
These guides assume that you can read HTML, CSS, JavaScript or TypeScript, and TSX. You do not need previous Tavo.js experience. The examples use TypeScript, npm, and the generated project defaults.
- Node.js 20.19+ or 22.12+ for development, builds, and server rendering.
- A terminal and editor with TypeScript support.
- A modern browser for the client application and development tools.
**New to Tavo.js? Build one app first**
Follow Build your first Tavo.js app for a cumulative path with a working checkpoint after every edit. The topic guides also stand alone when you need a specific answer.
## The Tavo.js application model
Files define the route tree. Loaders and middleware prepare request-scoped data. Models and controllers own interactive behavior, while stores share client state. The same route tree can render on the server, in the browser, or as static output.
Tavo.js UI supplies accessible interface primitives and a project-owned theme. The framework CLI creates, inspects, validates, builds, and packages the application. The guides keep these responsibilities visible so convenience never hides the runtime boundary.
## Build one app from start to finish
Follow the dedicated first-app tutorial when you want one cumulative path with a checkpoint after every change. Its navigation stays focused on the app you are building.
[
## Build your first Tavo.js app
Build a Project Dashboard while learning routes, data, state, navigation, CSR, and SSR.Read guide →
](/docs/getting-started/first-app)
## Start with a focused guide
[
## Installation
Create an application, add Tavo.js UI, and run it locally.Read guide →
](/docs/getting-started/installation)[
## Project structure
Understand the generated files and choose where feature code belongs.Read guide →
](/docs/getting-started/project-structure)
## Build
[
## Pages and layouts
Turn files into typed routes, shared shells, and route-specific pending and error views.Read guide →
](/docs/getting-started/pages-and-layouts)[
## Linking and navigating
Move through the route tree with responsive client navigation.Read guide →
](/docs/getting-started/linking-and-navigating)[
## Server and client execution
Choose the correct runtime and keep hydration deterministic.Read guide →
](/docs/getting-started/server-and-client-execution)[
## Components, controllers, and stores
Start with plain components, then add local behavior and shared state only when needed.Read guide →
](/docs/getting-started/components-controllers-and-stores)
## Data
[
## Fetching data
Load route-critical data with useful pending, error, and cancellation behavior.Read guide →
](/docs/getting-started/fetching-data)[
## Mutating data
Validate, authorize, and commit server changes through actions.Read guide →
](/docs/getting-started/mutating-data)[
## Caching
Choose static output and safe response-cache boundaries.Read guide →
](/docs/getting-started/caching)[
## Revalidating
Refresh cached routes by time or application-owned tags.Read guide →
](/docs/getting-started/revalidating)[
## Error handling
Choose route, global, and component-level recovery boundaries.Read guide →
](/docs/getting-started/error-handling)
## Interface
[
## CSS and Tavo.js UI
Combine local styles, design tokens, and accessible components.Read guide →
](/docs/getting-started/css-and-tavo-ui)[
## Image optimization
Deliver stable, responsive local and remote images.Read guide →
](/docs/getting-started/images)[
## Font loading
Load local or external fonts without avoidable layout shift.Read guide →
](/docs/getting-started/fonts)[
## Metadata and social images
Describe routes for search engines, browsers, and social previews.Read guide →
](/docs/getting-started/metadata-and-social-images)
## Server
[
## Server routes
Expose deliberate HTTP endpoints through the plugin boundary.Read guide →
](/docs/getting-started/server-routes)[
## Middleware
Run request-aware checks before route resolution.Read guide →
](/docs/getting-started/middleware)
## Ship
[
## Testing and diagnostics
Catch failures at the fastest useful layer.Read guide →
](/docs/getting-started/testing-and-diagnostics)[
## Deploying
Match the build output to the runtime your application needs.Read guide →
](/docs/getting-started/deploying)[
## Upgrading
Update the Tavo.js packages together and verify production behavior.Read guide →
](/docs/getting-started/upgrading)
# Build your first Tavo.js app
> Build a Project Dashboard while learning routes, loaders, local component state, controllers, Tavo.js UI, client navigation, CSR, and SSR.
Canonical page: https://tavojs.dev/docs/getting-started/first-app
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: quickstart
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- You can read HTML, CSS, TypeScript, and TSX; no Tavo.js experience is required.
- Node.js 20.19+ or 22.12+, npm, a terminal, and a code editor are available.
- Ports 5173 and 4174 are available, or you will use the URLs printed by the development servers.
## Outcomes
- Create a Tavo.js project and identify what the generated starter demonstrates.
- Add a file route with browser-safe loader data.
- Add local interactive state and named behavior with createTavo and TavoController.
- Verify the same route in client and server development modes.
## Before you begin
Quickstart · Runs in Browser, Server, Build
### You will learn
- Create a `Tavo.js` project and identify what the generated starter demonstrates.
- Add a file route with browser-safe loader data.
- Add local interactive state and named behavior with `createTavo` and `TavoController`.
- Verify the same route in client and server development modes.
### Starting point
- You can read HTML, CSS, `TypeScript`, and TSX; no `Tavo.js` experience is required.
- `Node.js 20.19+ or 22.12+`, `npm`, a terminal, and a code editor are available.
- Ports 5173 and 4174 are available, or you will use the URLs printed by the development servers.
## Start with the application model
A `Tavo.js` application is a `TypeScript` program with a file-based route tree. A page file owns one URL, its loader prepares route data, and its TSX component renders that data. `createTavo` adds local reactive state when a view becomes interactive, while a `TavoController` gives that interaction named behavior.
You will build one feature in small working increments. Run the checkpoint after each increment before continuing; when something breaks, you then know which edit introduced it.
**What this quickstart leaves for later**
Actions, authentication, caching, custom plugins, and deployment are separate guides. The first app stays focused on the route and rendering model you need before those topics.
## 1. Create and inspect the starter
Create a project named `project-dashboard`, install its declared dependencies, and start the client development server.
Run in `Terminal`
**Run Terminal**
```bash
node --version
npx @tavojs/cli create app project-dashboard
cd project-dashboard
npm install
npm run dev
```
**Checkpoint**
**Expected:** The URL printed by Vite opens the generated counter. Increment, decrement, reset, and theme controls update immediately.
**If it does not work:** If Vite rejects the Node version, install Node.js 20.19+ or 22.12+, remove the incomplete node\_modules directory, and run npm install again.
The starter already demonstrates `Tavo.js` local state: `src/pages/index.tsx` defines a model and updates it with `model.patch()`. Keep that page and `src/styles.css`; the next steps add a second route without discarding the working starter. After trying the controls, stop the server with Ctrl+C before installing the next dependency.
## 2. Add Tavo.js UI through its plugin
Install the component package and ask its bundled `tavo-ui` command to create the project theme configuration. `@tavojs/ui-cli` is already a dependency of `@tavojs/ui`; do not install or version it separately.
Run in `Terminal`
**Run Terminal**
```bash
npm install @tavojs/ui
npx tavo-ui web init
```
Merge `tavoUi()` into the configuration generated by the `Tavo.js` CLI. Keep `src/styles.css`, diagnostics, build settings, and every existing plugin. The UI plugin generates and injects theme variables during development and production builds, so this path does not add a generated theme file to `cssEntries`.
Merge into `tavo.config.ts`
**Merge tavo.config.ts**
```ts
import { defineConfig } from "@tavojs/core/config";
import { tavoUi } from "@tavojs/ui/plugin";
export default defineConfig({
pagesDir: "src/pages",
cssEntries: ["src/styles.css"],
diagnostics: {
devOverlay: true,
traces: false,
},
plugins: [tavoUi()],
});
```
**Checkpoint**
**Run Terminal**
```bash
npx tavo-ui web check
npm run typecheck
npm run dev
```
**Expected:** The theme check reports success, TypeScript completes without errors, and Vite starts the client application again.
**If it does not work:** If tavo-ui cannot find a config, confirm that the init command created tavo-ui.config.ts in the project root. If the page loses its starter styling, restore src/styles.css in cssEntries.
## 3. Create the projects route
Create `src/pages/projects/index.tsx`. The path below is special: every non-underscore `TypeScript` or TSX module inside `src/pages` is treated as a route module. This file therefore creates `/projects`.
Start with a plain TSX function. A page does not need a model, controller, store, or loader until its behavior requires one.
Create `src/pages/projects/index.tsx`
**Create src/pages/projects/index.tsx**
```tsx
import { Card, Page, Stack, Text } from "@tavojs/ui";
export default function ProjectsPage() {
return (
Project dashboard
Make the learning path clear for a new Tavo.js developer.
);
}
```
**Checkpoint**
**Expected:** With npm run dev still running, http://localhost:5173/projects shows the Project dashboard heading and one Documentation card.
**If it does not work:** If Tavo.js reports that a page module has no default component, check the default export and confirm the file is named index.tsx inside src/pages/projects.
## 4. Load browser-safe route data
Create a normal source module for data that is safe to include in a browser bundle. This example contains public sample records—no database client, secret, session, or private API key.
Create `src/data/projects.ts`
**Create src/data/projects.ts**
```ts
export type Project = {
id: string;
name: string;
summary: string;
active: boolean;
};
export const projects: Project[] = [
{
id: "docs",
name: "Documentation",
summary: "Make the learning path clear for a new Tavo.js developer.",
active: true,
},
{
id: "website",
name: "Website",
summary: "Ship the next public release.",
active: true,
},
{
id: "mobile",
name: "Mobile",
summary: "Plan a later product experiment.",
active: false,
},
];
```
Add a named `load` export to the functional route module. Its result becomes `data` for the default page component. Because this loader imports browser-safe data, it can run during both client navigation and SSR.
Replace `src/pages/projects/index.tsx`
**Replace src/pages/projects/index.tsx**
```tsx
import type { PageProps } from "@tavojs/core/router";
import { Card, Grid, Page, Stack, Text } from "@tavojs/ui";
import { projects, type Project } from "../../data/projects";
type ProjectsData = { projects: Project[] };
export function load(): ProjectsData {
return { projects };
}
export default function ProjectsPage({ data }: PageProps) {
return (
Project dashboard
{data?.projects.map((project) => (
{project.summary}
))}
);
}
```
**Checkpoint**
**Expected:** The projects route now shows Documentation, Website, and Mobile cards. A refresh keeps the same result.
**If it does not work:** If the import cannot be resolved, check that projects.ts is under src/data and that the route uses the relative path ../../data/projects.
## 5. Add local state and a controller
The filter belongs only to the dashboard currently on screen, so local model state is the narrowest owner. The controller gives each transition a clear name: `showAll()` and `showActive()` patch the model, while the view reads `state.filter` and delegates button events to those methods.
Create the component below. Pass `ProjectDashboardController` as the third `createTavo` type argument and register it with the `controller` option. `Tavo.js` creates the controller with access to the component model and disposes it with the mounted component.
Create `src/components/ProjectDashboard.tsx`
**Create src/components/ProjectDashboard.tsx**
```tsx
import { createTavo, TavoController } from "@tavojs/core";
import { Button, Card, Grid, Inline, Stack, Text } from "@tavojs/ui";
import type { Project } from "../data/projects";
type ProjectFilter = "all" | "active";
type ProjectDashboardProps = { projects: Project[] };
type ProjectDashboardState = { filter: ProjectFilter };
class ProjectDashboardController extends TavoController {
showAll() {
this.model.patch({ filter: "all" });
}
showActive() {
this.model.patch({ filter: "active" });
}
}
export const ProjectDashboard = createTavo<
ProjectDashboardProps,
ProjectDashboardState,
ProjectDashboardController
>({
model: () => ({ filter: "all" }),
controller: ProjectDashboardController,
view: ({ props, state, controller }) => {
const visibleProjects = props.projects.filter(
(project) => state.filter === "all" || project.active,
);
return (
Showing {visibleProjects.length} of {props.projects.length} projects
{visibleProjects.map((project) => (
{project.summary}
{project.active ? "Active" : "Planned"}
))}
);
},
});
```
Replace the route one final time so the loader still owns project data, the dashboard model owns the current filter, and the controller owns the filter-changing behavior. This separation prevents request data from being copied into a global client store.
Replace `src/pages/projects/index.tsx`
**Replace src/pages/projects/index.tsx**
```tsx
import type { PageProps } from "@tavojs/core/router";
import { Page, Stack, Text } from "@tavojs/ui";
import { ProjectDashboard } from "../../components/ProjectDashboard";
import { projects, type Project } from "../../data/projects";
type ProjectsData = { projects: Project[] };
export function load(): ProjectsData {
return { projects };
}
export default function ProjectsPage({ data }: PageProps) {
return (
Project dashboard
Route data supplies the projects. Local model state controls the
visible filter.
);
}
```
**Checkpoint**
**Expected:** All projects shows three cards. Active projects shows two cards, updates the count, and marks the selected button with aria-pressed.
**If it does not work:** If clicking does nothing, confirm the component registers ProjectDashboardController, passes it as the third createTavo type argument, and calls the controller methods from the button handlers.
## 6. Link the starter to the dashboard
Create a small navigation component with the `Tavo.js` router `Link`. A link represents a destination and lets the pages runtime coordinate history, focus, scroll, loaders, and cancellation.
Create `src/components/ProjectDashboardLink.tsx`
**Create src/components/ProjectDashboardLink.tsx**
```tsx
import { Link } from "@tavojs/core/router";
export function ProjectDashboardLink() {
return Open the project dashboard;
}
```
Update the generated home page in two small places while keeping the counter and its original styles intact. First, add this import beside the existing imports at the top of the file.
Update `src/pages/index.tsx`
**Patch src/pages/index.tsx**
```tsx
import { ProjectDashboardLink } from "../components/ProjectDashboardLink";
```
Next, replace the existing one-line `app-footer` with this expanded footer.
Update `src/pages/index.tsx`
**Patch src/pages/index.tsx**
```tsx
```
**Checkpoint**
**Expected:** Open http://localhost:5173, activate Open the project dashboard, and confirm the URL changes to /projects without a full document reload.
**If it does not work:** If the link renders but navigation fails, verify that it imports Link from @tavojs/core/router and uses the to prop rather than href.
## 7. Verify client and server rendering
You have been using client-side rendering (CSR): Vite serves the document shell and the browser resolves the route. Stop that server, then start `Tavo.js`'s server-side rendering (SSR) development mode.
Run in `Terminal`
**Run Terminal**
```bash
# Stop the CSR server with Ctrl+C, then run:
npm run dev:ssr
# In another terminal, inspect the server-rendered HTML:
curl http://localhost:4174/projects
```
**Checkpoint**
**Expected:** The curl response contains Project dashboard, and http://localhost:4174/projects hydrates into the same interactive filter in the browser.
**If it does not work:** If port 4174 is occupied, use the URL printed by the SSR server for both the browser and curl. A hydration warning usually means the first browser render differs from the server HTML.
## What you built
Inspect `Final project tree`
**Reference Final project tree**
```text
project-dashboard/
├── src/
│ ├── components/
│ │ ├── ProjectDashboard.tsx local filter model, controller, and UI
│ │ └── ProjectDashboardLink.tsx client navigation
│ ├── data/
│ │ └── projects.ts browser-safe sample data
│ ├── pages/
│ │ ├── index.tsx generated starter plus dashboard link
│ │ └── projects/
│ │ └── index.tsx /projects route and loader
│ └── styles.css preserved starter styles
├── tavo-ui.config.ts project theme input
└── tavo.config.ts framework and UI plugin configuration
```
The route owns URL-level data and composition. The dashboard model owns state used only by that mounted view, and its controller owns named state transitions. The data module is ordinary browser-safe `TypeScript`. The UI plugin owns theme generation while the existing stylesheet remains part of the application.
## Next steps
[
## Project structure
Learn which folders Tavo.js enforces and where application code belongs.Read guide →
](/docs/getting-started/project-structure)[
## Server and client execution
Decide when data may run in both environments and when it must stay on the server.Read guide →
](/docs/getting-started/server-and-client-execution)[
## Fetching data
Replace local sample data with abortable network requests and explicit failure handling.Read guide →
](/docs/getting-started/fetching-data)
# Installation
> Create a Tavo.js application, add the integrated UI system, and run both client and server development modes.
Canonical page: https://tavojs.dev/docs/getting-started/installation
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- Node.js 20.19+ or 22.12+, npm, a terminal, and a TypeScript-capable editor.
- An empty parent directory in which the CLI may create project-dashboard.
## Outcomes
- Create and run the generated Tavo.js starter.
- Install @tavojs/ui and initialize its bundled theme CLI.
- Merge the UI plugin without removing starter styles or diagnostics.
- Start both client and server development modes.
## Check the development environment
Use `Node.js` 20.19 or newer (or 22.12 or newer) for `Tavo.js` development, builds, and server rendering. The application uses `TypeScript` and `npm` by default, but the CLI can record another package manager when your team has standardized on one.
Run in `Terminal`
**Run Terminal**
```bash
node --version
npm --version
```
## Create the application
The create command writes the route directory, application entry, `TypeScript` and Vite configuration, development scripts, starter page, and agent-facing project guidance. Install dependencies after reviewing the target directory.
Open the URL printed by Vite and try the counter. Stop the development server with Ctrl+C before continuing to the installation commands below.
Run in `Terminal`
**Run Terminal**
```bash
npx @tavojs/cli create app project-dashboard
cd project-dashboard
npm install
npm run dev
```
**Generated files are a starting point**
The CLI protects existing files by default. Use force only when you have deliberately reviewed the files that will be replaced.
**Checkpoint**
**Expected:** The URL printed by Vite opens the generated counter, and its counter and theme controls update immediately.
**If it does not work:** If Vite rejects the Node version, install Node.js 20.19+ or 22.12+, reinstall the dependencies, and rerun npm run dev. Stop the working server with Ctrl+C before continuing.
## Use the team's package manager
The rest of this guide uses `npm` and runs project-local tools with `npx tavo` and `npx tavo-ui`. The equivalents are `pnpm exec tavo`, `yarn tavo`, or `bunx tavo` for the framework and `pnpm exec tavo-ui`, `yarn tavo-ui`, or `bunx tavo-ui` for UI tooling.
## Add Tavo.js UI
`Tavo.js` Framework owns routing, data, rendering, and application behavior. `Tavo.js` UI supplies interface components and project theme tooling. Install `@tavojs/ui`, then initialize its theme configuration.
The `tavo-ui` command comes with `@tavojs/ui` through its package dependencies. Do not install or independently version `@tavojs/ui-cli` in the application.
Run in `Terminal`
**Run Terminal**
```bash
npm install @tavojs/ui
npx tavo-ui web init
```
## Enable plugin-based theme generation
Merge `tavoUi()` into the generated configuration. Keep `src/styles.css`, diagnostics, build options, and every existing plugin. During development and production builds, the plugin reads the default `tavo-ui.config.ts` file and injects the generated theme variables; no generated theme file belongs in `cssEntries` for this setup.
Merge into `tavo.config.ts`
**Merge tavo.config.ts**
```ts
import { defineConfig } from "@tavojs/core/config";
import { tavoUi } from "@tavojs/ui/plugin";
export default defineConfig({
pagesDir: "src/pages",
cssEntries: ["src/styles.css"],
diagnostics: {
devOverlay: true,
traces: false,
},
plugins: [tavoUi()],
});
```
**Merge; do not replace**
The example repeats the generated pages, styles, and diagnostics to make the final shape clear. If your configuration already contains other settings or plugins, preserve them and add `tavoUi()` to the existing plugins array.
## Verify both rendering modes
The standard development command is the fastest client iteration loop. Use SSR development as soon as you add server loaders, actions, middleware, sessions, or request-time metadata.
Run in `Terminal`
**Run Terminal**
```bash
npm run dev
# Stop the CSR server, then start SSR development:
npm run dev:ssr
```
**Use the project-local CLI**
Keep stable commands in package scripts or run them through `npx` so contributors and CI use the version declared by the application.
**Checkpoint**
**Run Terminal**
```bash
npx tavo-ui web check
npm run typecheck
```
**Expected:** The theme check passes, TypeScript reports no errors, and both development modes print a local URL.
**If it does not work:** If starter styling disappears, restore src/styles.css in cssEntries. If the theme config is missing, rerun the init command from the project root.
Learn more
- [Server and client execution](/docs/getting-started/server-and-client-execution) — Understand which code runs in each rendering mode and where private work belongs.
## Next steps
[
## Project structure
Learn what the generated files own.Read guide →
](/docs/getting-started/project-structure)[
## Tavo.js UI installation
Review theme and package options in depth.Read guide →
](/docs/ui/installation)
# Project structure
> Understand the generated application shape and organize route, feature, server, and asset code deliberately.
Canonical page: https://tavojs.dev/docs/getting-started/project-structure
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: concept
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A project created with `tavo create app`.
- The project is open in an editor so you can compare its tree with this guide.
## Outcomes
- Recognize the files created by the Tavo.js CLI.
- Know which files inside src/pages become public routes.
- Keep private modules inside the enforced src/server boundary.
- Choose simple shared and feature folders without inventing framework rules.
## Read the generated project
The scaffold keeps configuration at the project root and application code under `src`. `Tavo.js` gives two source locations special safety or routing behavior: `src/pages` defines the route tree, and `src/server` is blocked from client bundles. Other source folders are application organization.
Inspect `Project tree`
**Reference Project tree**
```text
project-dashboard/
├── public/ static browser assets
├── src/
│ ├── pages/ file-based routes and layouts
│ ├── server/ enforced server-only modules, when needed
│ ├── components/ reusable interface and behavior
│ ├── store/ shared client state
│ ├── main.tsx browser bootstrap
│ └── styles.css application-wide styles
├── index.html document shell
├── tavo.config.ts framework and plugin settings
├── tavo-ui.config.ts project theme input, after UI setup
├── tsconfig.json TypeScript contract
└── vite.config.ts build integration
```
## Know which names affect routing
Every non-underscore `TypeScript` or TSX module inside `src/pages` is treated as a route module. Normal folders add URL segments. Brackets create parameters, three dots create catch-all parameters, and parentheses group files without adding a public segment. A `_layout.tsx` file wraps its descendants.
Do not place ordinary helpers, services, stores, or components in `src/pages` with a normal filename: `Tavo.js` will try to load them as routes. Move them to another source folder, or use an underscore-prefixed private file only for a small route-local helper.
Inspect `src/pages`
**Reference src/pages**
```text
index.tsx → /
projects/index.tsx → /projects
projects/[id].tsx → /projects/:id
docs/[[...slug]].tsx → /docs/*?slug
(marketing)/about.tsx → /about
projects/_layout.tsx → wraps /projects and descendants
projects/_format.ts → private helper; not a route
projects/format.ts → route module; do not use as a helper
```
Learn more
- [Pages and layouts](/docs/getting-started/pages-and-layouts) — See these route conventions in typed page and layout examples.
## Decide where each file belongs
`src/pages` and `src/server` have framework meaning. Names such as `components` and `features` do not: they are folders you create to make browser-safe and shared code easy to find.
Start with the simple structure below. A route file loads the data for its URL and assembles the page. Move code out of that file when it becomes reusable or when it must stay on the server.
- Used by one route? Keep it next to that route until the file becomes hard to read.
- Used by several routes? Move it to `src/components`, `src/features`, or another clearly named shared folder.
- Uses a database, secret, session, or private API key? Put it in `src/server`. `Tavo.js` treats that directory as server-only and blocks imports that reach a client bundle.
- Needed directly by the browser, such as an image or font? Put it in `public`; `public/logo.svg` is available at `/logo.svg`.
- Found in `.tavo/build` or `.tavo/generated`? Do not edit it. `Tavo.js` recreates generated files.
Inspect `One possible structure`
**Reference One possible structure**
```text
src/
├── pages/
│ └── projects/
│ └── [id].tsx route for /projects/:id
├── features/
│ └── projects/
│ └── ProjectCard.tsx UI used by project routes
├── components/
│ └── PageHeader.tsx UI reused across the app
└── server/
└── projects.ts database code; never imported by browser code
public/
└── logo.svg available in the app as /logo.svg
```
**You do not need the final folder structure on day one**
Begin with `src/pages`, a shared `src/components` folder, and `src/server` if the application has private server code. Add feature folders only when they make files easier to find.
Learn more
- [Server and client execution](/docs/getting-started/server-and-client-execution#server-boundary) — Learn what a server-only loader is and how to protect private dependencies.
- [Fetching data](/docs/getting-started/fetching-data#route-loaders) — Learn how page and layout loaders provide route data.
- [Mutating data](/docs/getting-started/mutating-data) — Learn how server actions validate, authorize, and commit changes.
- [Middleware](/docs/getting-started/middleware#server-only) — Learn how server-only middleware handles sessions and private request checks.
## Check the route boundary
Use the CLI to inspect the route tree after adding or moving page files. The output should contain only URLs that the application intentionally exposes.
Run in `Terminal`
**Run Terminal**
```bash
npx tavo routes
npm run typecheck
```
**Checkpoint**
**Expected:** The route list contains the intended pages and no helper filenames, and TypeScript reports no errors.
**If it does not work:** If a helper appears as a route, move it outside src/pages or prefix its filename with an underscore. If a client build reports a server-only import, move the private call behind a server loader, action, middleware, or server route.
## Next steps
[
## Pages and layouts
Turn the route tree into application UI.Read guide →
](/docs/getting-started/pages-and-layouts)[
## Create and generate
Use the CLI to add files that follow these conventions.Read guide →
](/docs/cli/create-and-generate)
# Pages and layouts
> Turn the file tree into typed routes, nested application shells, and route-specific loading and error states.
Canonical page: https://tavojs.dev/docs/getting-started/pages-and-layouts
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Turn the file tree into typed routes, nested application shells, and route-specific loading and error states.
## Create a functional page
A page module describes one public route. The default function renders resolved page props, while optional named exports such as `load`, `pending`, `error`, `action`, `middleware`, `head`, and rendering or caching exports add route behavior. The file path remains the source of the public URL.
Create `src/pages/projects/[id].tsx`
**Create src/pages/projects/[id].tsx**
```tsx
import type { PageLoadContext, PageProps } from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Project = { id: string; name: string };
type ProjectParams = { id: string };
export async function load({
params,
signal,
url,
}: PageLoadContext): Promise {
const response = await fetch(
new URL(`/api/projects/${encodeURIComponent(params.id)}`, url),
{ signal },
);
if (!response.ok) throw new Error("Project could not be loaded");
return response.json() as Promise;
}
export default function ProjectPage({
data,
}: PageProps) {
return (
{data?.name}
);
}
```
**Keep route work abortable**
Navigation supplies an `AbortSignal` to loaders. Pass it to `fetch` and downstream clients so an abandoned route cannot publish stale data.
## Add route-aware typing when it helps
`defineRoutePage` is an optional helper for keeping the loader, page component, and other route behaviors in one typed object. Its path literal infers dynamic parameters such as `id` and connects the loader data type to the default page.
The file tree still owns routing. The helper does not register or rename a route, so keep its path literal aligned with the page filename and confirm the result with `npx tavo routes`.
Create `src/pages/projects/[id].tsx`
**Create src/pages/projects/[id].tsx**
```tsx
import { defineRoutePage } from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Project = { id: string; name: string };
export default defineRoutePage<"/projects/[id]", Project>("/projects/[id]", {
load: async ({ params, signal, url }) => {
const response = await fetch(
new URL(`/api/projects/${encodeURIComponent(params.id)}`, url),
{ signal },
);
if (!response.ok) throw new Error("Project could not be loaded");
return response.json() as Promise;
},
default: function ProjectPage({ data }) {
return (
{data?.name}
);
},
});
```
**The functional module remains the default**
Use named exports for the clearest general-purpose page contract. Reach for `defineRoutePage` when its route-aware inference or grouped object form improves the page; the CLI offers it explicitly through `npx tavo generate page 'projects/[id]' --typed-route`.
## Share UI with layouts
A `_layout.tsx` file wraps every descendant page. Layouts compose from the root toward the leaf, so the root can own global navigation while a projects layout owns project-specific navigation and shared data.
Create `src/pages/projects/_layout.tsx`
**Create src/pages/projects/_layout.tsx**
```tsx
import type { Child } from "@tavojs/core";
import { Link } from "@tavojs/core/router";
import { Box, Stack } from "@tavojs/ui";
export default function ProjectsLayout({ children }: { children?: Child }) {
return (
Projects
{children}
);
}
```
## Add route-specific loading and error UI
Export `pending` when a client navigation should replace the previous page with immediate route-specific feedback while the page loader runs. Export `error` when that page should own a contextual loader-failure view. Both exports are normal `Tavo.js` components and can be functions or components created with `createTavo()`.
On client navigation, `Tavo.js` resolves middleware and layout loaders first, renders `pending` inside the matched layouts, and then runs the page loader. The completed default page and its controller do not mount until the loader succeeds.
- `PagePendingProps` provides the target pathname, params, resolved layout layers, and layer data. Page loader data is unavailable because it is still loading.
- `PageErrorProps` adds `data` and `error` for the failed route. Present a safe message instead of rendering raw server details.
- Use `aria-busy="true"` and a useful accessible label for pending content. Use `role="alert"` or an equivalent announcement strategy for errors.
Create `src/pages/dashboard.tsx`
**Create src/pages/dashboard.tsx**
```tsx
import type {
PageErrorProps,
PageLoadContext,
PagePendingProps,
PageProps,
} from "@tavojs/core/router";
import { Page, Stack, Text } from "@tavojs/ui";
type DashboardData = {
projects: Array<{ id: string; name: string }>;
summary: { active: number };
};
export function pending({ pathname }: PagePendingProps) {
return (
Loading dashboard…
Preparing projects and their summary.
);
}
export function error({ pathname }: PageErrorProps) {
return (
Could not load the dashboard
The data for {pathname} is unavailable. Try again.
);
}
export async function load({
signal,
url,
}: PageLoadContext): Promise {
const [projectsResponse, summaryResponse] = await Promise.all([
fetch(new URL("/api/projects", url), { signal }),
fetch(new URL("/api/projects/summary", url), { signal }),
]);
if (!projectsResponse.ok || !summaryResponse.ok) {
throw new Error("Dashboard data could not be loaded");
}
return {
projects: await projectsResponse.json(),
summary: await summaryResponse.json(),
} as DashboardData;
}
export default function DashboardPage({ data }: PageProps) {
return (
Active projects: {data?.summary.active ?? 0}
{data?.projects.map((project) => (
{project.name}
))}
);
}
```
**Pending UI is a browser-navigation state**
Normal SSR, static prerendering, prefetching, and navigation satisfied from a fresh route cache wait for or already have the completed route, so they do not render the `pending` export.
## Use the route shape that matches the URL
Generate route types with a production build before relying on newly added dynamic paths throughout the application.
- Use `[id]` for one required segment.
- Use `[...slug]` for one-or-more catch-all segments.
- Use `[[...slug]]` when the catch-all route also owns its base URL.
- Use `(group)` folders to organize routes or apply alternate layouts without changing the URL.
## Provide route-level fallbacks
Add `src/pages/404.tsx` for URLs that do not match a route. Keep the message clear and provide a path back into the application.
Create `src/pages/404.tsx`
**Create src/pages/404.tsx**
```tsx
import { Link } from "@tavojs/core/router";
import { Page, Stack, Text } from "@tavojs/ui";
export const head = Page not found;
export default function NotFoundPage() {
return (
Page not found
The page may have moved or the address may be incorrect.
Return home
);
}
```
A page's `error` export is the first fallback for its page-loader failure. Add `src/pages/_error.tsx` as the application-wide fallback for routes that do not provide one. Show a stable recovery message without rendering server details, tokens, response bodies, or stack traces.
Create `src/pages/_error.tsx`
**Create src/pages/_error.tsx**
```tsx
import { Link } from "@tavojs/core/router";
import { Page, Stack, Text } from "@tavojs/ui";
export const head = Something went wrong;
export default function RouteErrorPage() {
return (
We could not load this page
Try again, or return home if the problem continues.
Return home
);
}
```
**Expected failures still belong in the page**
A rejected form is normal application state. Use `notFound()` when route data is absent and should render `404.tsx`; it bypasses both route and global error views. Use a component `ErrorBoundary` for render-subtree failures.
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## Core pages and layouts
Review the full route and layout contract.Read guide →
](/docs/core/pages-and-layouts)[
## Linking and navigating
Connect the routes with client navigation.Read guide →
](/docs/getting-started/linking-and-navigating)
# Linking and navigating
> Move between routes while preserving browser history, focus, scroll position, and useful loading feedback.
Canonical page: https://tavojs.dev/docs/getting-started/linking-and-navigating
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Move between routes while preserving browser history, focus, scroll position, and useful loading feedback.
## Use links for application URLs
The Core Link component performs client navigation for internal destinations and keeps normal anchor behavior available to the browser. Use links for destinations and buttons for actions.
Create `src/components/ProjectNavigation.tsx`
**Create src/components/ProjectNavigation.tsx**
```tsx
import { Link } from "@tavojs/core/router";
import { Inline } from "@tavojs/ui";
export function ProjectNavigation() {
return (
All projects
Active projects
);
}
```
## Let the pages runtime own navigation behavior
The pages runtime matches the route, resolves middleware and loaders, updates metadata, announces the new page, restores focus, and coordinates scroll. When the target page exports `pending`, the URL changes first, middleware and layout loaders resolve, and the pending component renders inside those layouts while the page loader runs.
- Use scroll={false} only when preserving the current position is part of the interaction.
- Prefer route links over a second top-level standalone router.
- Without a `pending` export, the previous page remains visible and the route content region is marked busy until the target resolves.
- New routes start at the top, hashes target an element, and browser back or forward restores saved positions.
**Navigation is interruptible**
A later navigation aborts loader and middleware work from the route it replaces, removes its pending component, and prevents obsolete data or errors from replacing the active route. Treat `AbortError` as control flow rather than an application failure.
## Prefetch when intent is clear
`prefetchRoute` can resolve a likely destination before the click without changing the URL or rendering the route's `pending` component. Use it for focused or hovered high-probability links, not every route in the application. `getRouteStatus` exposes idle, loading, prefetching, ready, redirecting, and error states without duplicating the router state machine.
Create `src/navigation/prefetch-projects.ts`
**Create src/navigation/prefetch-projects.ts**
```ts
import { getRouteStatus, prefetchRoute } from "@tavojs/core/router";
export async function prefetchProjects() {
await prefetchRoute("/projects");
return getRouteStatus("/projects");
}
```
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## Core routing
See route status, prefetch, and standalone-router guidance.Read guide →
](/docs/core/routing)[
## Fetching data
Understand the work that navigation owns.Read guide →
](/docs/getting-started/fetching-data)
# Server and client execution
> Choose where route work runs, protect private dependencies, and keep the first browser render consistent with server HTML.
Canonical page: https://tavojs.dev/docs/getting-started/server-and-client-execution
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: concept
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Choose where route work runs, protect private dependencies, and keep the first browser render consistent with server HTML.
## Understand the rendering model
`Tavo.js` renders the same TSX component model on the server and in the browser. In an SSR runtime, routes render on the server by default and hydrate in the browser. A route can opt into client-only rendering when its initial HTML is not important.
Create `src/pages/browser-only.tsx`
**Create src/pages/browser-only.tsx**
```tsx
import { Page } from "@tavojs/ui";
export const render = "csr";
export default function BrowserOnlyPage() {
return Rendered in the browser;
}
```
Stage
Route component
Universal loader
Server-only loader
Initial CSR request
Browser
Browser
Skipped
Initial SSR request
Server, then browser hydration
Server; result is serialized
Server; safe result is serialized
Later client navigation
Browser
Browser
Skipped
SSG build
Build process
Build process
Build process
ISR refresh
Server runtime
Server runtime
Server runtime
- SSR renders for the current request.
- CSR sends the document shell and resolves the route in the browser.
- SSG prerenders selected static routes during the build.
- ISR serves cached SSR output and refreshes it after a revalidation interval.
- A route `pending` export can appear during browser route resolution, but normal SSR and static generation wait for the completed page or its error view.
## Keep private work behind a server boundary
`defineServerLoader` marks route data that must only resolve on the server. `Tavo.js` also enforces `src/server/**` as a server-only module boundary: the client build fails if code from that directory reaches its module graph. Put databases, sessions, private API clients, and secret-bearing code there, then reach it through server loaders, actions, middleware, or server routes.
A server-only loader is skipped during browser route resolution. Its SSR result is available during initial hydration, but a later client navigation needs safe client auth state or a server endpoint such as /api/me when it must refresh that data.
Create `src/pages/account.tsx`
**Create src/pages/account.tsx**
```tsx
import { defineServerLoader } from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Account = { name: string };
export const load = defineServerLoader(async ({ request, signal }) => {
const { getAccount, readSession } = await import("./session.server");
const session = await readSession(request);
if (!session) throw new Error("Authentication required");
return getAccount(session.accountId, { signal });
});
export default function AccountPage({ data }: { data?: Account }) {
return (
Welcome, {data?.name}
);
}
```
**Do not pass secrets through page data**
Loader output is serialized for hydration. Return the minimum safe fields the interface needs, never session objects, credentials, private headers, or raw database records.
## Keep hydration deterministic
The browser hydrates against the route data and HTML resolved by the server. A hydration warning means the initial client tree did not reproduce that output.
- Guard browser-only globals such as window and `localStorage`.
- Avoid time, randomness, and locale differences in the initial render.
- Use deterministic IDs from framework helpers.
- Test the production SSR build, not only the development server.
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## SSR and hydration
Configure route output and debug hydration.Read guide →
](/docs/core/ssr-and-hydration)[
## Security
Protect server modules, sessions, hosts, and redirects.Read guide →
](/docs/core/security)
# Components, controllers, and stores
> Give renderable state, application behavior, and shared client state one clear owner each.
Canonical page: https://tavojs.dev/docs/getting-started/components-controllers-and-stores
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: concept
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Give renderable state, application behavior, and shared client state one clear owner each.
## Begin with a plain component
A component that only transforms props into TSX should be a normal function. It is easier to read, test, and reuse because it owns no hidden behavior.
Create `src/components/ProjectList.tsx`
**Create src/components/ProjectList.tsx**
```tsx
import { Stack, Text } from "@tavojs/ui";
type ProjectSummary = { id: string; name: string };
export function ProjectList({ projects }: { projects: ProjectSummary[] }) {
return (
{projects.map((project) => (
{project.name}
))}
);
}
```
## Add a local model for local interaction
When that mounted component needs renderable state, wrap it with `createTavo` and add a model. The filter below affects only this project list, so a global store would make ownership less clear.
Create `src/components/ProjectFilter.tsx`
**Create src/components/ProjectFilter.tsx**
```tsx
import { createTavo } from "@tavojs/core";
import { Button, Inline, Stack, Text } from "@tavojs/ui";
type Project = { id: string; name: string; active: boolean };
type ProjectFilterProps = { projects: Project[] };
type ProjectFilterState = { filter: "all" | "active" };
export const ProjectFilter = createTavo(
{
model: () => ({ filter: "all" }),
view: ({ props, state, model }) => {
const visibleProjects = props.projects.filter(
(project) => state.filter === "all" || project.active,
);
return (
{visibleProjects.map((project) => (
{project.name}
))}
);
},
},
);
```
## Add a controller only for coordinated behavior
A `Tavo.js` component has a required view and optional model and controller. Add a controller when behavior needs lifecycle, cleanup, services, routing, subscriptions, or side effects. Keep small event handlers that only patch local model state in the view.
## Share client state through named stores
A global store is appropriate when several mounted components or routes need the same client preference. Keep mutation methods beside the state so callers express intent rather than coordinating partial writes.
Create `src/components/ActiveProjectButton.tsx`
**Create src/components/ActiveProjectButton.tsx**
```tsx
import { createTavo, defineGlobalStore, TavoController } from "@tavojs/core";
import { Button } from "@tavojs/ui";
type FilterState = {
status: "all" | "active";
showActive(): void;
};
export const projectFilters = defineGlobalStore(
"project-filters",
(set) => ({
status: "all",
showActive: () => set({ status: "active" }),
}),
);
class FilterController extends TavoController {
showActive() {
projectFilters.getState().showActive();
}
}
export const ActiveProjectButton = createTavo({
controller: FilterController,
view: ({ controller }) => (
),
});
```
**Global does not mean request-scoped**
Global stores are process-wide during SSR. Never put the current user, tenant, token, session, or permissions in them; return safe request data from a loader instead.
## Make cleanup part of the behavior
Use `onInit` for subscriptions and initial model work, `onMount` or `onLayout` for DOM-dependent work, `afterRender` for post-commit behavior, and `onPropsChange` for explicit prop reactions. Return cleanup or register it with the controller so unmounting ends listeners and work.
- Prefer controller helpers for events, store selection, resize, and intersection observation.
- Subscribe to the smallest store value the component needs.
- Keep request data in page or layout props instead of mirroring it into a process-wide store.
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## MVC components
Learn controller lifecycle and component ownership in depth.Read guide →
](/docs/core/mvc)[
## Stores
Choose store scope, subscriptions, and derived state.Read guide →
](/docs/core/stores)
# Fetching data
> Load route-critical data, design pending and error views, stream secondary content, and cancel obsolete work.
Canonical page: https://tavojs.dev/docs/getting-started/fetching-data
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Load route-critical data, design pending and error views, stream secondary content, and cancel obsolete work.
## Load data the route needs
A page or layout loader runs during route resolution. Its return value becomes page data and is serialized into SSR output, so the browser can hydrate without immediately repeating the request. The page's default component and controller do not mount until its loader completes.
Create `src/pages/dashboard.tsx`
**Create src/pages/dashboard.tsx**
```tsx
import type {
PageErrorProps,
PageLoadContext,
PagePendingProps,
PageProps,
} from "@tavojs/core/router";
import { Page, Stack, Text } from "@tavojs/ui";
type DashboardData = {
projects: Array<{ id: string; name: string }>;
summary: { active: number };
};
export function pending({ pathname }: PagePendingProps) {
return (
Loading dashboard…
Preparing projects and their summary.
);
}
export function error({ pathname }: PageErrorProps) {
return (
Could not load the dashboard
The data for {pathname} is unavailable. Try again.
);
}
export async function load({
signal,
url,
}: PageLoadContext): Promise {
const [projectsResponse, summaryResponse] = await Promise.all([
fetch(new URL("/api/projects", url), { signal }),
fetch(new URL("/api/projects/summary", url), { signal }),
]);
if (!projectsResponse.ok || !summaryResponse.ok) {
throw new Error("Dashboard data could not be loaded");
}
return {
projects: await projectsResponse.json(),
summary: await summaryResponse.json(),
} as DashboardData;
}
export default function DashboardPage({ data }: PageProps) {
return (
Active projects: {data?.summary.active ?? 0}
{data?.projects.map((project) => (
{project.name}
))}
);
}
```
**Check every response**
fetch resolves for HTTP error statuses. Check `response.ok` and turn expected failures into explicit page data before attempting to read a success shape.
## Design the route while its loader is unresolved
The example above exports `pending` for immediate feedback during active browser resolution and `error` for a contextual page-loader failure. `Tavo.js` renders either component inside the target route's resolved layouts.
The browser changes the URL, runs route middleware, resolves layout loaders, renders `pending`, and then runs the page loader. Success replaces it with the default page; failure replaces it with the page's `error` component.
Resolution path
Pending export
Visible result
Initial CSR resolution
Rendered while the page loader runs
Completed page or route error
Later client navigation
Rendered after layout loaders resolve
Completed page or route error
Normal SSR or static generation
Not rendered
Server waits for the completed page or error
Prefetch or fresh route-cache hit
Not rendered
No visible route replacement during prefetch
- `PagePendingProps` contains `pathname`, `params`, `layers`, and `layerData`; page loader data is intentionally unavailable.
- `PageErrorProps` contains those route fields plus `data` and `error`.
- Layout data is available because `Tavo.js` resolves target layout loaders before showing the page pending component.
- If the page has no `pending` export, the previous page remains visible and the route content region is marked busy.
**Route pending or component resource?**
Use a route `pending` export when the page cannot render meaningfully before its loader completes. Use `createResource()` when the page shell can render immediately and only one section owns the asynchronous work.
## Know when the loader runs
A universal loader follows the route resolver. The table shows why its imports and return value must be safe in every environment that can execute or receive them.
Entry path
Where load runs
What the browser receives
Direct CSR visit
Browser
No serialized loader result
Direct SSR visit
Server
Result is serialized for hydration
Hydration
Browser reuses server data
No immediate repeat
Later client navigation
Browser
Fresh result belongs to that navigation
## Choose the narrowest data owner
Avoid copying loader results into global stores. Page props already keep request data scoped to the navigation that produced it.
- Use a page loader when the route cannot render meaningfully without the data.
- Use a layout loader for request data shared by its descendant routes.
- Use `createResource` for component-scoped browser data that can load independently.
- Use lazy when the asynchronous work is loading a component implementation.
## Stream secondary server content
Resolve data required for navigation, SEO, and the primary shell in the loader. Put slower optional server work behind Deferred boundaries with a stable ID, useful fallback, timeout behavior, and an error fallback.
Promise-backed Deferred content progressively patches an SSR stream. For browser-only asynchronous work, use a loader, resource, controller, or store instead.
## Pass cancellation through every layer
Navigation owns loader and middleware signals. Replacing a navigation aborts its route work, removes its pending component, and prevents obsolete data or errors from replacing the active route. A resource owns its current load. Deferred work belongs to its supplied signal or render lifecycle. Forward the signal to fetch, database wrappers that support it, and other cancellable clients.
- Treat `AbortError` as normal control flow.
- Do not publish results after their owner has been replaced.
- Use transactions or idempotency for side effects because cancellation cannot undo a committed mutation.
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## Data loading and middleware
Compare loaders, middleware, resources, and cancellation.Read guide →
](/docs/core/data-and-middleware)[
## Streaming and async work
Design deferred boundaries and timeout behavior.Read guide →
](/docs/core/streaming-and-async)[
## Error handling
Choose between route errors, the global error page, and component boundaries.Read guide →
](/docs/getting-started/error-handling)
# Mutating data
> Handle server changes through validated, authorized route actions with explicit pending and failure states.
Canonical page: https://tavojs.dev/docs/getting-started/mutating-data
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Handle server changes through validated, authorized route actions with explicit pending and failure states.
## Put server mutations in route actions
A route action handles non-GET requests in SSR mode. It receives a standard Request, so the handler can read JSON or form data, authorize the caller, perform the change, and return JSON, status, headers, or a redirect.
Create `src/pages/projects/new.tsx`
**Create src/pages/projects/new.tsx**
```ts
import {
createServerFormAction,
createTavo,
TavoController,
} from "@tavojs/core";
import { defineValidatedAction } from "@tavojs/core/dev";
import {
Button,
Field,
FormControl,
Page,
Stack,
Text,
TextInput,
} from "@tavojs/ui";
type ProjectInput = { name: string };
const projectSchema = {
parse(input: unknown): ProjectInput {
const name = String((input as { name?: unknown }).name ?? "").trim();
if (!name) throw new Error("Project name is required");
return { name };
},
};
export const action = defineValidatedAction(
projectSchema,
async ({ input, request }) => {
const { requireProjectPermission, saveProject } =
await import("./project-store.server");
await requireProjectPermission(request);
const project = await saveProject(input);
return { redirect: `/projects/${project.id}` };
},
);
type ProjectFormState = { pending: boolean; error: string };
class ProjectFormController extends TavoController {
formAction = createServerFormAction("/projects/new");
async submit(event: Event) {
event.preventDefault();
this.model.patch({ pending: true, error: "" });
const result = await this.formAction.submit(
event.currentTarget as HTMLFormElement,
);
this.model.patch({
pending: false,
error:
result.status === "error"
? result.error instanceof Error
? result.error.message
: "Project could not be created"
: "",
});
}
}
export default createTavo<{}, ProjectFormState, ProjectFormController>({
model: () => ({ pending: false, error: "" }),
controller: ProjectFormController,
view: ({ state, controller }) => (
New project
void controller?.submit(event)}
>
{state.error ? (
{state.error}
) : null}
),
});
```
## Separate browser and server responsibilities
Phase
Runtime
Responsibility
Render form
Browser and SSR
Show fields, current errors, and pending state.
Submit
Browser
Serialize input and send the non-GET request.
Route action
Server
Validate, authenticate, authorize, and commit.
Apply response
Browser
Show field errors, success data, or follow a redirect.
Static-only deployment
Unavailable
Use a server runtime or external API for mutations.
## Validate before business logic
`defineValidatedAction` accepts Standard Schema and common parse-compatible validators. Invalid input receives a structured 400 response before the mutation handler runs.
**Validation is not authorization**
A valid payload can still come from the wrong user. Authenticate and verify permissions inside every server action before changing state.
## Keep the mutation order predictable
- Parse and validate the input shape.
- Authenticate the request and authorize the specific resource operation.
- Apply origin, CSRF, and idempotency rules appropriate to the endpoint.
- Commit the database or external side effect.
- Return only safe data, an explicit error shape, or a same-origin redirect.
## Show pending and expected failures
Use `createServerFormAction` when a controller should own browser submission state for a route action. Disable duplicate submission while pending, associate validation messages with their fields, and announce the result without moving focus unexpectedly.
**Design retries before enabling them**
A network timeout does not prove that a server mutation failed. Use idempotency keys for operations that users or infrastructure may safely retry.
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## Actions, forms, and validation
Review action responses, validation, and security order.Read guide →
](/docs/core/actions-and-forms)[
## Revalidating
Refresh cached reads after a successful change.Read guide →
](/docs/getting-started/revalidating)
# Caching
> Prebuild stable routes and cache safe SSR responses without crossing user or request boundaries.
Canonical page: https://tavojs.dev/docs/getting-started/caching
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Prebuild stable routes and cache safe SSR responses without crossing user or request boundaries.
## Mark routes that can be static
Export prerender when a route can safely reuse its rendered output. For a dynamic path, `generateStaticParams` lists the concrete URLs the production build should prerender.
Static generation renders the initial `createTavo` view during the build; the browser can then hydrate it and let its controller own later interactions. In this example, every visitor receives the same safe article HTML while save state remains local to the hydrated component.
Create `src/pages/articles/[slug].tsx`
**Create src/pages/articles/[slug].tsx**
```tsx
import { createTavo, TavoController } from "@tavojs/core";
import type { PageLoadContext, PageProps } from "@tavojs/core/router";
import { Button, Page, Stack, Text } from "@tavojs/ui";
type Article = { title: string };
type ArticleParams = { slug: string };
type ArticlePageState = { saved: boolean };
const articles: Record = {
"getting-started": { title: "Getting started" },
"release-notes": { title: "Release notes" },
};
function getArticle(slug: string): Article {
const article = articles[slug];
if (!article) throw new Error(`Unknown article: ${slug}`);
return article;
}
export const prerender = true;
export function generateStaticParams(): ArticleParams[] {
return [{ slug: "getting-started" }, { slug: "release-notes" }];
}
export function load({ params }: PageLoadContext): Article {
return getArticle(params.slug);
}
class ArticlePageController extends TavoController {
toggleSaved() {
this.model.patch((state) => ({ saved: !state.saved }));
}
}
export default createTavo<
PageProps,
ArticlePageState,
ArticlePageController
>({
model: () => ({ saved: false }),
controller: ArticlePageController,
view: ({ props, state, controller }) => (
{props.data?.title}
),
});
```
**Static does not mean non-interactive**
Keep the initial model deterministic and safe to share. Browser-only controller behavior can begin after hydration, but request users, sessions, permissions, and personalized defaults must not enter the prerendered output.
## Cache only shareable output
The route cache stores rendered HTML and resolved route data. Requests carrying Cookie or Authorization bypass static caching because their output may be user-specific.
- Do not mark personalized pages static.
- Use vary only for a small, deliberate set of request headers that genuinely change safe output.
- Keep request users, sessions, tenants, and permissions out of global stores and module variables.
**Static is a data contract**
The route must remain safe to share for every request that resolves to the same cache key. Treat that requirement as part of the route design, not only a performance switch.
## Separate rendering from caching
Mode or request
Where rendering happens
Cache behavior
CSR
Browser on each visit
Normal HTTP asset caching only
SSR
Server for each request
None unless route or adapter enables it
SSG
Build process
Generated HTML and route data
ISR
Server after expiry
Serve cached output, then replace it
Cookie or Authorization request
Server
Shared static cache is bypassed
## Choose a production cache adapter
The Node runtime uses a bounded process-local memory cache by default. It is useful for one process, but it is not shared across replicas and disappears when the process restarts. Supply a cache adapter when the application needs shared persistence or coordinated invalidation.
## Inspect what the build decided
`tavo` build prints each route render mode and static policy. Use the JSON report and generated route manifest in CI when static output is an application requirement.
Run in `Terminal`
**Run Terminal**
```bash
tavo build --report-json
tavo inspect route /articles/getting-started --json
```
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## SSR and hydration
Compare SSR, CSR, SSG, and revalidated output.Read guide →
](/docs/core/ssr-and-hydration)[
## Revalidating
Keep cached output fresh.Read guide →
](/docs/getting-started/revalidating)
# Revalidating
> Refresh cached routes by time or application-owned tags while keeping deployment behavior explicit.
Canonical page: https://tavojs.dev/docs/getting-started/revalidating
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Refresh cached routes by time or application-owned tags while keeping deployment behavior explicit.
## Refresh after a time interval
A numeric `revalidate` value automatically enables cached SSR and gives the cached output a lifetime in seconds. After it expires, the next resolution produces fresh output and replaces the stored entry.
Choose the route-aware helper when you want typed path parameters and the route contract in one object. Choose named exports when you want each behavior visible at module scope. Both examples describe the same revalidated project route.
Create `src/pages/projects/[id].tsx`
**Create src/pages/projects/[id].tsx**
```tsx
import { defineRoutePage } from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Project = { id: string; name: string };
function getProject(id: string): Project {
return { id, name: `Project ${id}` };
}
export default defineRoutePage<"/projects/[id]", Project>("/projects/[id]", {
revalidate: 300,
cacheTags: ({ params }) => ["projects", `project:${params.id}`],
load: ({ params }) => getProject(params.id),
default: function ProjectPage({ data }) {
return (
{data?.name}
);
},
});
```
**Create src/pages/projects/[id].tsx**
```tsx
import type { PageLoadContext, PageProps } from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Project = { id: string; name: string };
type ProjectParams = { id: string };
function getProject(id: string): Project {
return { id, name: `Project ${id}` };
}
export const revalidate = 300;
export const cacheTags = ({ params }: PageLoadContext) => [
"projects",
`project:${params.id}`,
];
export function load({ params }: PageLoadContext): Project {
return getProject(params.id);
}
export default function ProjectPage({
data,
}: PageProps) {
return (
{data?.name}
);
}
```
**Revalidation already enables caching**
Do not add `prerender = true` merely to enable cached SSR. Reserve `prerender = true` for build-time static HTML without timed revalidation; inside `defineRoutePage`, use its equivalent, `static: true`.
## Name related cached output
`cacheTags` attaches application-owned names to a route entry. Tags can be static or derived from route parameters, which lets one mutation target a collection, one record, or both.
- Use stable domain names such as projects and project:42.
- Do not put secrets or personal data into a tag.
- Keep tag production beside the route data contract so readers can see what invalidates it.
## Invalidate through the runtime boundary
`PagesRuntime` exposes synchronous tag invalidation for its resolved-route cache. The Node request handler exposes asynchronous `invalidateCache` for both runtime and static adapter entries. Custom cache adapters may also implement `invalidateTags`.
Create `src/server/invalidate-project.ts`
**Create src/server/invalidate-project.ts**
```ts
type CacheInvalidator = {
invalidateCache(tags: string[]): Promise;
};
export async function invalidateProjectCache(
requestHandler: CacheInvalidator,
projectId: string,
) {
return requestHandler.invalidateCache(["projects", `project:${projectId}`]);
}
```
**Invalidation must reach every replica**
Process-local invalidation affects only that process. In a multi-instance deployment, connect tag invalidation to the shared cache or platform coordination mechanism.
## Invalidate only after the mutation commits
Trigger invalidation after the database or external write succeeds. If the mutation and cache live in different systems, record enough information to retry failed invalidation without repeating the business change.
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## Deployment
Connect cache behavior to the selected runtime.Read guide →
](/docs/core/deployment)[
## Testing and diagnostics
Verify cache hits, misses, and invalidation.Read guide →
](/docs/getting-started/testing-and-diagnostics)
# Error handling
> Choose route and global error views, model normal failures as state, and contain exceptions at the nearest useful boundary.
Canonical page: https://tavojs.dev/docs/getting-started/error-handling
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Choose route and global error views, model normal failures as state, and contain exceptions at the nearest useful boundary.
## Return expected failures explicitly
Validation failures, empty results, rejected permissions, and unavailable remote services can be normal application outcomes. Give them typed data or response shapes so the page can explain what happened and offer a useful next action.
- Associate form errors with the field that needs attention.
- Use status codes and JSON shapes consistently across route actions and server routes.
- Call `notFound()` when missing route data should render `404.tsx`; unmatched URLs use the same page.
## Contain unexpected render failures
`ErrorBoundary` protects a component subtree and renders fallback UI when rendering fails. Place boundaries around areas that can recover independently instead of replacing the entire application for one failed panel.
Create `src/components/ProjectPanel.tsx`
**Create src/components/ProjectPanel.tsx**
```tsx
import { ErrorBoundary, type Child } from "@tavojs/core";
import { Button, Stack, Text } from "@tavojs/ui";
export function ProjectPanel({ children }: { children?: Child }) {
return (
(
Projects could not be displayed.
)}
>
{children}
);
}
```
**Do not expose private error details**
Show a stable user-facing message and send diagnostic details to controlled logging. Stack traces, request headers, loader data, and tokens do not belong in rendered fallback UI.
## Choose the closest route error view
A page can export an `error` component for its own loader failures. It receives `PageErrorProps`, including the target pathname, params, data and error fields, resolved layers, and layer data, and it renders inside the matched layout chain.
If the target page does not export `error`, `Tavo.js` uses `src/pages/_error.tsx` as the application-wide fallback. A layout-loader failure enters this error-resolution path instead of showing the page pending component with invalid layout data.
- Use a page `error` export when the recovery message or next action is specific to that route.
- Keep `_error.tsx` broad enough to handle any remaining page or layout loader failure.
- `notFound()` bypasses both route and global error views and renders `404.tsx` with status 404.
- Render failures belong in the nearest component `ErrorBoundary` because they happen after route resolution.
**Treat the error value as sensitive**
Use the `error` prop for safe classification and controlled diagnostics. Do not render stack traces, request headers, tokens, raw response bodies, or private loader data.
## Design asynchronous failure states
Resources expose idle, loading, success, and error states. Deferred boundaries accept error and timeout fallbacks. A retry should start a new owned operation and replace the old signal rather than reusing abandoned work.
- Do not report deliberate cancellation as an error.
- Set timeouts for optional dependencies that should not hold a stream open.
- Instrument the route, loader, action, cache, or boundary phase without recording private payloads.
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## Testing and diagnostics
Capture route, hydration, and runtime failures at the correct layer.Read guide →
](/docs/core/testing-and-diagnostics)[
## Streaming and async work
Add deferred error and timeout behavior.Read guide →
](/docs/core/streaming-and-async)
# CSS and Tavo.js UI
> Combine application styles, locally scoped CSS, design tokens, and accessible interface components.
Canonical page: https://tavojs.dev/docs/getting-started/css-and-tavo-ui
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Combine application styles, locally scoped CSS, design tokens, and accessible interface components.
## Give each style layer one job
Use application-wide CSS for document defaults and product-wide rules, CSS Modules for component-specific presentation, `Tavo.js` UI props for supported component behavior, and generated theme tokens for shared design decisions.
- List deliberate global entry files in `tavo.config.ts` `cssEntries`.
- Name local files \*`.module.css` so class names remain scoped. Install sass-embedded before choosing \*`.module.scss` for application code.
- Keep third-party global styles in one documented application entry.
- Use tokens instead of repeating product colors, spacing, radii, and typography values.
## Compose with Tavo.js UI
`Tavo.js` UI components provide semantic defaults, states, responsive props, and theme integration. They do not own your application data or business behavior; route modules, controllers, and stores remain responsible for those concerns.
Create `src/pages/projects/index.tsx`
**Create src/pages/projects/index.tsx**
```tsx
import { Button, Card, Grid, Page, Text } from "@tavojs/ui";
export default function ProjectsPage() {
return (
Refresh the getting started guide.
);
}
```
## Generate a project-owned theme
`tavo-ui.config.ts` describes the light and dark brand colors, default mode, numeric scale, typography, and token overrides the product needs. The `Tavo.js` UI plugin resolves that input and injects generated variables during development and production builds.
Merge into `tavo-ui.config.ts`
**Merge tavo-ui.config.ts**
```ts
import type { TavoUiThemeConfig } from "@tavojs/ui";
export default {
$schema: "./node_modules/@tavojs/ui/schema.json",
defaultTheme: "system",
color: {
light: { primary: "#3157d5" },
dark: { primary: "#9bb1ff" },
},
scale: { radius: 8 },
} satisfies TavoUiThemeConfig;
```
**The application owns the theme**
Regenerate theme output through the UI CLI instead of editing generated variables by hand. Commit the input configuration that expresses the product decision.
## Keep responsive behavior and order predictable
Responsive values are mobile-first: base applies everywhere, then larger breakpoint values override it. Import order still controls global CSS, so keep one deliberate sequence and avoid relying on incidental module discovery.
- Use component responsive props for supported layout behavior.
- Use CSS Modules for product-specific selectors and states.
- Use the exported breakpoint mixins when custom CSS must align with the UI system.
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## Tavo.js UI overview
Choose theming, responsive, composition, and accessibility guides.Read guide →
](/docs/ui)[
## Design tokens
Use generated tokens in product-specific styles.Read guide →
](/docs/ui/tokens)
# Image optimization
> Render stable responsive images and opt into tightly controlled server optimization for local or remote assets.
Canonical page: https://tavojs.dev/docs/getting-started/images
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Render stable responsive images and opt into tightly controlled server optimization for local or remote assets.
## Describe the image layout
The Image component emits an img element with responsive optimizer URLs during SSR. Supply accurate dimensions to reserve space, a meaningful alt description, widths that match the design, and sizes that explain the rendered width to the browser.
Create `src/components/ProjectHero.tsx`
**Create src/components/ProjectHero.tsx**
```tsx
import { Image } from "@tavojs/core";
import { Page } from "@tavojs/ui";
export function ProjectHero() {
return (
);
}
```
**Priority is exceptional**
Mark only an above-the-fold image that materially affects the initial view as priority. Lazy loading remains the better default for the rest of the page.
## Serve local assets from public
Files under public have root-relative browser URLs. Keep the source path inside that directory so the optimizer cannot read arbitrary server files. Use unoptimized when the source is already transformed or the deployment has no image optimizer.
## Allow remote sources narrowly
Remote optimization is disabled until the application enables it and supplies an HTTPS allowlist. Restrict hostnames and paths to the product's actual media origins.
Merge into `tavo.config.ts`
**Merge tavo.config.ts**
```ts
import { defineConfig } from "@tavojs/core/config";
export default defineConfig({
ssr: {
images: {
allowRemote: true,
remotePatterns: [
{
protocol: "https:",
hostname: "media.example.com",
pathname: "/projects/",
},
],
},
},
});
```
**Remote media is a server security boundary**
Keep the allowlist exact, preserve request-size and timeout limits, and do not enable insecure HTTP sources in production.
## Install the production transformer when needed
The SSR optimizer uses the optional sharp dependency for transformations. Install it in the application that runs the server, then verify the /\_tavo/image endpoint in production preview and on the selected platform.
Run in `Terminal`
**Run Terminal**
```bash
npm install sharp
npm run build
npm run preview:ssr
```
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## SEO, assets, and styling
Review Image, Font, Script, and metadata boundaries.Read guide →
](/docs/core/seo-assets-and-styling)[
## Security
Harden remote media and production SSR configuration.Read guide →
](/docs/core/security)
# Font loading
> Load self-hosted or external fonts deliberately while controlling privacy, fallback behavior, and layout stability.
Canonical page: https://tavojs.dev/docs/getting-started/fonts
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Load self-hosted or external fonts deliberately while controlling privacy, fallback behavior, and layout stability.
## Prefer a self-hosted font when possible
Place licensed font files under public and render Font from a root layout head export. The component creates the preload and @font-face rules for SSR and CSR, while a CSS variable makes the family available to the theme or application styles.
Create `src/pages/_layout.tsx`
**Create src/pages/_layout.tsx**
```tsx
import { Font, type Child } from "@tavojs/core";
export const head = (
);
export default function RootLayout({ children }: { children?: Child }) {
return <>{children}>;
}
```
**Use the smallest useful font set**
A variable WOFF2 file often replaces several individual weights. Preload only fonts required by the initial view; extra preloads compete with more important resources.
## Choose a visible fallback strategy
font-display controls whether text waits for the custom file. swap keeps content visible immediately, while optional lets the browser retain the fallback on slow connections. Choose a fallback family with similar metrics to reduce movement when the custom font appears.
## Treat external fonts as third-party requests
Font can emit stylesheet and preconnect links for an external provider. That request exposes the visitor's network metadata to another origin and adds a runtime dependency, so confirm the privacy and availability tradeoff before using it.
- Preconnect only to origins used by the selected stylesheet.
- Keep integrity and CSP requirements aligned with the provider.
- Prefer self-hosting when privacy, reliability, or offline behavior matters.
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## SEO, assets, and styling
See the complete document-asset model.Read guide →
](/docs/core/seo-assets-and-styling)[
## Theming
Connect the font variable to the product theme.Read guide →
](/docs/ui/theming)
# Metadata and social images
> Describe each route for browsers, search engines, link previews, and assistive navigation.
Canonical page: https://tavojs.dev/docs/getting-started/metadata-and-social-images
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Describe each route for browsers, search engines, link previews, and assistive navigation.
## Keep metadata beside the route
Export a static head value when every visit shares the same metadata. Use a head function when the title, description, status, or social image depends on route parameters or loader data.
Create `src/pages/projects/[id].tsx`
**Create src/pages/projects/[id].tsx**
```tsx
import { Seo } from "@tavojs/core";
import type { PageLoadContext, PageProps } from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Project = { name: string; summary: string; image: string };
type ProjectParams = { id: string };
export function load({ params }: PageLoadContext): Promise {
return getProject(params.id);
}
export function head({ data, params }: PageLoadContext & { data?: Project }) {
return (
);
}
export default function ProjectPage({
data,
}: PageProps) {
return (
{data?.name}
);
}
async function getProject(id: string): Promise {
return {
name: `Project ${id}`,
summary: "A project managed with Tavo.js.",
image: "https://media.example.com/projects/social.png",
};
}
```
## Publish the fields consumers need
- Use one specific title and plain-language description per public route.
- Set a canonical URL when multiple URLs can expose equivalent content.
- Use robots, `noIndex`, and `noFollow` intentionally for non-public routes.
- Provide Open Graph and Twitter images with stable HTTPS URLs and useful source dimensions.
- Set theme-color when the browser chrome should follow the product theme.
## Serve social images as public assets
Store static share images under public or point Seo at a trusted media origin. `Tavo.js` does not generate Open Graph artwork from route code; create the image through your asset pipeline and publish the final URL.
**Metadata is server output**
Request-time metadata can use safe loader data, but it must not expose session values, private record fields, internal hostnames, or untrusted raw HTML.
## Verify the rendered document
Inspect the production SSR HTML, not only the browser DOM after navigation. Confirm the title, canonical URL, description, robots policy, and social tags for static, dynamic, success, and not-found routes.
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## SEO, assets, and styling
Review head exports, Seo, and document assets.Read guide →
](/docs/core/seo-assets-and-styling)[
## Deploying
Confirm canonical origins and metadata in production.Read guide →
](/docs/getting-started/deploying)
# Server routes
> Expose deliberate HTTP endpoints through the plugin server boundary using standard web requests and responses.
Canonical page: https://tavojs.dev/docs/getting-started/server-routes
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Expose deliberate HTTP endpoints through the plugin server boundary using standard web requests and responses.
## Choose a server route for an HTTP endpoint
Use a page action when a mutation belongs to one application route. Use a plugin server route for a reusable API, webhook, health check, or integration endpoint that should respond before page rendering.
**Server routes require an SSR runtime**
Static hosting and a client-only Vite server cannot execute these handlers. Run the generated `.tavo/build/server/start.mjs` entry with Node.
## Register methods in a plugin
Declare every endpoint in the plugin manifest, then implement the same endpoint keys in a lazy server phase. Handlers receive a standard Request and return a terminal Response.
Create `project-api.ts`
**Create project-api.ts**
```ts
import { definePlugin, definePluginPhase } from "@tavojs/core/plugin";
export const projectApi = definePlugin({
id: "@project/api",
version: "1.0.0",
apiVersion: 1,
manifest: {
endpoints: [
{
id: "summary",
methods: ["GET"],
match: { kind: "exact", path: "/api/projects/summary" },
},
{
id: "collection",
methods: ["GET", "POST"],
match: { kind: "exact", path: "/api/projects" },
},
{
id: "project",
methods: ["GET"],
match: { kind: "subtree", path: "/api/projects" },
},
],
exposure: [
{
target: "server",
from: "/api/projects",
to: "/api/projects",
reason: "Expose the documented project API.",
},
],
},
server: () =>
definePluginPhase({
endpoints: {
summary: async () => {
const { projectSummary } = await import("./project-store.server");
return Response.json(await projectSummary());
},
collection: async ({ request }) => {
const store = await import("./project-store.server");
if (request.method === "POST") {
await store.requireProjectPermission(request);
const input = store.parseProjectInput(await request.json());
return Response.json(await store.createProject(input), {
status: 201,
});
}
const status =
new URL(request.url).searchParams.get("status") ?? "all";
return Response.json(await store.listProjects(status));
},
project: async ({ request }) => {
const { getProject } = await import("./project-store.server");
const id = decodeURIComponent(
new URL(request.url).pathname.split("/").at(-1)!,
);
const project = await getProject(id);
return project
? Response.json(project)
: Response.json({ error: "Project not found" }, { status: 404 });
},
},
}),
});
```
## Enable the plugin
Defining a plugin does not expose it by itself. Add it to the top-level plugins array in `tavo.config.ts`; the manifest exposure maps its declared server path onto the application's public URL tree.
Merge into `tavo.config.ts`
**Merge tavo.config.ts**
```ts
import { defineConfig } from "@tavojs/core/config";
import { projectApi } from "./project-api";
export default defineConfig({
pagesDir: "src/pages",
plugins: [projectApi],
});
```
## Keep route matching explicit
Use exact matchers for individual URLs and subtree matchers for a deliberate route tree. Read query values and dynamic path pieces from `request.url`, validate them before use, and keep response formats stable for clients.
- Return 405 when an endpoint does not support the incoming method.
- Return deliberate content-type and cache headers.
- Keep database and secret-bearing clients in server-only modules.
- Bound request bodies and remote work with deployment-appropriate limits.
## Preserve the request security boundary
Unsafe methods validate their origin by default. Disable that check only for independently authenticated endpoints such as verified webhooks, and authenticate and authorize every protected operation inside the handler.
**A route is reachable without your UI**
Treat every server route and page action as a public network surface. Client-side visibility and button state are not access control.
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## Plugins and localization
Understand plugin services, ordering, routes, and request scope.Read guide →
](/docs/core/plugins-and-localization)[
## Security
Configure origins, hosts, redirects, bodies, and secrets.Read guide →
](/docs/core/security)
# Middleware
> Run fast request-aware checks before route loading and keep authentication data scoped to the active request.
Canonical page: https://tavojs.dev/docs/getting-started/middleware
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Run fast request-aware checks before route loading and keep authentication data scoped to the active request.
## Use middleware to control route flow
Middleware runs before loaders and rendering. It can allow navigation or redirect to another same-origin path with an optional redirect status. Use it for fast routing decisions rather than slow page data.
- Use page middleware when the check belongs to one route.
- Use layout middleware when descendants share the check.
- Register plugin middleware for application-wide integration behavior.
- Prefer hosting or platform redirect rules for unconditional redirects that do not need application request data.
## Keep session checks on the server
`defineServerMiddleware` prevents the check from running during browser navigation. It is the right boundary for `HttpOnly` cookies, server sessions, private clients, and permission prechecks.
Because it is skipped during SPA navigation, server-only middleware is not a client navigation gate. Pair it with safe hydrated auth state or client navigation handling, and keep the real authorization check in every protected loader, action, and server route.
Create `src/pages/account.tsx`
**Create src/pages/account.tsx**
```tsx
import { defineServerMiddleware } from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
export const middleware = defineServerMiddleware(async ({ request }) => {
const { readSession } = await import("./session.server");
const session = await readSession(request);
if (!session) return { redirect: "/login" };
});
export default function AccountPage() {
return (
Account
);
}
```
**Prechecks are not complete authorization**
Repeat resource-specific authorization inside the loader, action, or server route that reads or changes protected data. Middleware redirects improve flow; they do not make downstream code trusted.
## Know which middleware guards each entry path
Entry path
Page middleware
Server-only middleware
Final authorization
Direct SSR request
Runs
Runs
Repeat in loader or action
Hydration
Uses resolved route state
Does not rerun
Use only safe serialized identity
Later SPA navigation
Runs
Skipped
Protected loader or endpoint decides
Direct action or API request
Not a security boundary
Depends on registered request pipeline
Action or handler must enforce it
## Keep request data request-scoped
Read headers, cookies, and the URL from the supplied context. Pass only safe derived values through loader results or server services designed for the active request. Do not write the current user into a global store or module variable.
## Keep middleware fast and abortable
Middleware receives the navigation `AbortSignal`. Pass it to downstream work and stop promptly when navigation changes. Move route-critical data into a loader and avoid remote calls in middleware when a local cookie or claim is enough for the routing decision.
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## Data loading and middleware
Compare loader, resource, and middleware responsibilities.Read guide →
](/docs/core/data-and-middleware)[
## Mutating data
Enforce authorization again at the mutation boundary.Read guide →
](/docs/getting-started/mutating-data)
# Testing and diagnostics
> Check route behavior, project health, production rendering, and browser flows before a change reaches users.
Canonical page: https://tavojs.dev/docs/getting-started/testing-and-diagnostics
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Check route behavior, project health, production rendering, and browser flows before a change reaches users.
## Run the narrowest useful check
Use `TypeScript` and focused tests while editing, then widen the feedback loop as a change approaches release. `Tavo.js` diagnostics check the application shape and generated route contract without replacing tests for your product behavior.
Run in `Terminal`
**Run Terminal**
```bash
npx tsc --noEmit
npx tavo doctor
npx tavo check
npx tavo routes
```
**Read the first useful failure**
Later failures often follow from one invalid import, route, or configuration value. Fix the earliest actionable diagnostic, then run the checks again instead of treating the output as independent errors.
## Test routes without a browser
`createPagesTestHarness` resolves an in-memory page map with the same route conventions used by the application. Use it for fast route, loader, middleware, and error-path tests; keep DOM interaction and hydration assertions in a real browser.
Create `tests/pages/index.test.tsx`
**Create tests/pages/index.test.tsx**
```tsx
import { createPagesTestHarness } from "@tavojs/core/dev";
import { Text } from "@tavojs/ui";
const app = createPagesTestHarness({
"/src/pages/index.tsx": {
default: () => Project dashboard,
},
});
export async function homePageResolves() {
const result = await app.runtime.resolvePathAsync("/");
if (
result.status !== 200 ||
result.pathname !== "/" ||
result.route?.path !== "/"
) {
throw new Error("Home page did not resolve successfully");
}
}
```
## Test the production shape
A production build discovers routes, compiles client and server output, prerenders static pages, and reports `JavaScript` cost. SSR preview exercises the generated server entry rather than the development transform pipeline.
- Exercise direct requests, client navigation, actions, 404 and error pages, and hydration in browser tests.
- Start `npx` `tavo` preview `--ssr` in a separate terminal for manual SSR checks, or configure it as Playwright's `webServer`.
- Inspect `.tavo/generated/build-report.json` when a route or shared first load grows unexpectedly.
- Add `--max-first-load-js` and `--max-route-js` limits when CI should enforce bundle budgets.
Run in `Terminal`
**Run Terminal**
```bash
npx tavo build --report-json
npx playwright test
```
## Keep runtime evidence actionable
Development diagnostics identify route, mount, patch, and hydration phases. Start with the first server-client divergence and its DOM path; suppressing the warning leaves the underlying ownership or rendering mismatch in place.
Production logs and instrumentation should record route patterns, phases, timing, and stable diagnostic codes. Exclude request bodies, cookies, tokens, and loader results unless the application has an explicit redaction policy.
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## Testing and diagnostics
Configure framework diagnostics, instrumentation, and test helpers.Read guide →
](/docs/core/testing-and-diagnostics)[
## Build and preview
Inspect build reports, bundle budgets, and production previews.Read guide →
](/docs/cli/build-and-preview)
# Deploying
> Choose a runtime that matches the route contract, inspect generated output, and verify the real hosting boundary.
Canonical page: https://tavojs.dev/docs/getting-started/deploying
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Choose a runtime that matches the route contract, inspect generated output, and verify the real hosting boundary.
## Choose the runtime from application behavior
Static hosting can serve client-rendered assets and prerendered routes. Request-time features need a server runtime that can execute `Tavo.js`'s generated handler.
- Use static hosting for CSR and fully prerendered output that has no request-time loader, action, session, or middleware work.
- Use the generated Node production server for SSR, server loaders, route actions, sessions, and server routes.
- Treat revalidation and shared cache invalidation as deployment capabilities; the process-local default is neither durable nor coordinated across replicas.
**Static hosting cannot run server features**
A successful static upload does not make loaders or actions available. Select a server target whenever correctness depends on work performed for each request.
## Build once and inspect the artifacts
`tavo` build produces provider-neutral static and Node outputs. Do not edit generated files by hand or deploy stale artifacts.
Inspect `Generated output`
**Reference Generated output**
```text
.tavo/build/client/ static assets and prerendered HTML
.tavo/build/server/start.mjs generated Node production server
```
## Run the Node output
Start the generated server directly when request-time behavior is required. Static-only applications can publish the client directory to any static host.
- Keep authentication, loaders, actions, and business rules in application modules.
- Rebuild after every application change.
Run in `Terminal`
**Run Terminal**
```bash
npx tavo build
PORT=4174 node .tavo/build/server/start.mjs
```
## Verify the production boundary
Configure secrets, trusted hosts, canonical origin, request limits, cookies, CSP, remote image allowlists, and cache storage in the actual platform. Then smoke-test a direct SSR request, client navigation, mutation, error response, and static or revalidated route as applicable.
Protect runtime monitoring with `TAVO_MONITOR_TOKEN` and send the token through the Authorization header. Use `tavo` monitor against the deployed URL to inspect a snapshot or watch the application after release.
Run in `Terminal`
**Run Terminal**
```bash
npx tavo monitor --url https://app.example.com --token $MONITOR_TOKEN --once
```
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## Deployment
Review artifacts, cache boundaries, security, and runtime requirements.Read guide →
](/docs/core/deployment)[
## Deploy and monitor
Build provider-neutral output and protect production monitoring endpoints.Read guide →
](/docs/cli/deploy-and-monitor)
# Upgrading
> Update the framework, framework CLI, and UI as one reviewed change, then validate production behavior.
Canonical page: https://tavojs.dev/docs/getting-started/upgrading
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application created with the current CLI.
- Node.js 20.19+ or 22.12+ and the project dependencies installed.
## Outcomes
- Update the framework, framework CLI, and UI as one reviewed change, then validate production behavior.
## Review compatibility before changing versions
`Tavo.js` Framework 1.0 establishes the stable baseline. Read the Core and UI release notes for every version crossed, including configuration, generated-artifact, runtime, and experimental-API changes.
Core publishes machine-readable stable and experimental labels from `@tavojs/core`. Stable entry points follow semantic versioning from 1.0 onward; experimental entry points may evolve more quickly.
**Review experimental boundaries**
Check the release notes and every experimental entry point the application imports before updating the lockfile.
## Update the application packages together
Keep `@tavojs/core`, `@tavojs/cli`, and `@tavojs/ui` on compatible releases. The generated application installs `@tavojs/cli` as a development dependency and exposes its project-local `tavo` binary. The `tavo-ui` command is supplied through `@tavojs/ui`; do not install or upgrade `@tavojs/ui-cli` separately.
- With `pnpm`, add `@tavojs/core` and `@tavojs/ui`, then add `@tavojs/cli` with `--save-dev`.
- With Yarn, add the same three package names and keep `@tavojs/cli` in `devDependencies`.
- With Bun, add the same three package names and keep `@tavojs/cli` in `devDependencies`.
- Commit the manifest and lockfile together so CI and contributors install the reviewed dependency graph.
Run in `Terminal — npm`
**Run Terminal — npm**
```bash
npm install @tavojs/core@latest @tavojs/ui@latest
npm install --save-dev @tavojs/cli@latest
```
## Regenerate, then diagnose the project
Build once with the project-local CLI after installation so generated route types and manifests use the same version that will ship. Then diagnose the result. Treat doctor suggestions as reviewable changes; doctor `--fix-dry-run` reports proposed low-risk fixes without editing files.
Run in `Terminal`
**Run Terminal**
```bash
npx tavo build
npx tavo doctor --fix-dry-run
npx tavo check
npx tavo routes
npx tavo-ui web check
```
## Run the full release gate
Typecheck and test the application, then create a production report and exercise the SSR output in a browser. Give extra attention to routing, hydration, actions, caching, error boundaries, theme output, and any API called out by the release notes.
- Compare bundle reports and route manifests with the previous release when performance or static output is contractual.
- Run `npx` `tavo` preview `--ssr` in a separate terminal and smoke-test direct production-rendered requests before release.
- Deploy through the normal staging path and verify the generated output, secrets, caches, sessions, and monitor endpoint.
- Keep the previous lockfile and deploy artifact available until the new release passes production smoke checks.
Run in `Terminal`
**Run Terminal**
```bash
npx tsc --noEmit
npx tavo build --report-json
npx tavo verify --smoke --json
npx playwright test
```
## Checkpoint
**Checkpoint**
**Run Terminal**
```bash
npm run typecheck
```
**Expected:** TypeScript completes without errors. The route or component reproduces the behavior described in this guide.
**If it does not work:** Start with the first TypeScript error, confirm every shown file is in the documented location, and compare imports before changing runtime configuration.
## Next steps
[
## Core API reference
Review public entry points and the stability contract used by the application.Read guide →
](/docs/core/api)[
## Web UI CLI
Validate theme configuration and inspect the installed UI tooling.Read guide →
](/docs/ui/cli)
# Use Tavo.js with AI coding agents
> Connect MCP-compatible AI agents to versioned Tavo.js documentation, API metadata, component guidance, and optional read-only project inspection.
Canonical page: https://tavojs.dev/docs/mcp
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- Node.js 20.19+ or 22.12+.
- An MCP-compatible AI client.
- A Tavo.js application with a project-local @tavojs/cli package when project inspection is enabled.
## Outcomes
- Connect an AI client to @tavojs/mcp over local stdio.
- Choose between documentation-only and project-aware operation.
- Use Tavo.js knowledge and project tools within their read-only safety boundary.
## Give agents exact Tavo.js context
Tavo.js MCP gives an AI coding agent a structured, searchable view of Tavo.js. The agent can discover Framework APIs, Tavo.js UI components, semantic tokens, and task-oriented guides instead of relying on generic framework assumptions.
Local mode can also inspect and verify a Tavo.js application through its project-local CLI. Model Context Protocol (MCP) is the connection between the AI client and Tavo.js; it does not replace the AI client or edit source code itself.
**Read-only by design**
Every Tavo.js MCP capability is read-only. The AI host remains responsible for proposed code changes and user approval.
## Connect locally
Tavo.js MCP runs through npm without a global installation. Add the following server definition to the configuration used by your MCP client:
**Reference mcp.json**
```json
{
"mcpServers": {
"tavo": {
"command": "npx",
"args": [
"--yes",
"@tavojs/mcp",
"--project",
"/absolute/path/to/tavo-app"
]
}
}
}
```
MCP client configuration formats differ, but the command and arguments are portable. Replace the example path with an absolute path to the target application. The application must provide a project-local `@tavojs/cli` package, which exposes the `tavo` binary.
Omit `--project` and its path to keep the local server documentation-only.
[View @tavojs/mcp on npm](https://www.npmjs.com/package/@tavojs/mcp)
**Try the connection**
Ask your agent: “Use the Tavo.js MCP server to inspect this project, find the documented pattern for a settings page, and tell me which files you would change. Do not edit anything.”
## Choose a connection mode
Mode
Best for
Project data
Capabilities
Local stdio
Working inside a Tavo.js application
Remains on the developer's machine
Public knowledge plus project context, inspection, and verification
Self-hosted Streamable HTTP
Serving public Tavo.js knowledge to remote clients
No project data accepted
Public documentation, APIs, components, and prompts
Use local stdio for coding work because it can align guidance with the installed Tavo.js version and actual project structure. The npm package also includes a stateless HTTP transport for operators who want to host public documentation access. HTTP mode never accepts a project path or exposes project tools.
## What agents can use
### Public Tavo.js knowledge
Tool
Purpose
`search_tavo`
Search guides, APIs, components, and tokens by task.
`get_tavo_document`
Read one documentation record by stable ID.
`find_tavo_components`
Find UI components by intent, behavior, or accessibility need.
`lookup_tavo_api`
Find public Framework or Tavo.js UI symbols and package entry points.
### Local project context
Tool
Purpose
Safety boundary
`get_tavo_project_context`
Read compact, task-bounded project conventions.
Allowlisted agent-context CLI command
`inspect_tavo_project`
Inspect a route, component, store, file, plugin, or API.
Allowlisted inspect command and path validation
`verify_tavo_project`
Run diagnostics for the project or selected changed files.
Restricted verification without project scripts
The server also exposes versioned resources under `tavo://` and reusable prompts for building features, choosing UI components, and diagnosing projects. Tool responses default to 2,048 tokens and accept a `maxTokens` budget from 256 to 8,192.
## Keep project verification read-only
A normal project typecheck script is arbitrary project code: despite its name, it can write files, access the network, or perform another side effect.
**Restricted verification**
Tavo.js MCP delegates project verification only to `tavo verify --no-project-scripts --json`. This performs Tavo.js diagnostics without invoking package scripts and reports that project scripts were disabled.
The adapter rejects incompatible Framework CLI versions rather than silently falling back to verification that could execute project scripts. Documentation compatibility is reported alongside project tool results.
## How documentation reaches the server
The server bundles a validated public documentation snapshot and never reads the Tavo.js Website repository at runtime:
**Reference Reference snippet**
```text
Tavo.js Website editorial source
→ export a validated public documentation manifest
→ bundle and index the snapshot in @tavojs/mcp
→ expose version and content hash to the MCP client
```
Documentation updates require a new validated snapshot and MCP release. The runtime needs neither Website source nor Website credentials.
## Safety and privacy
- All MCP tools are read-only; the AI host remains responsible for changes and user approval.
- HTTP mode never accepts --project and cannot inspect a developer's application.
- Local inspection uses validated project-relative paths and only the allowlisted agent-context, inspect, and verify CLI machine commands.
- Internal documentation and credentials are excluded from the bundled public content manifest.
- Custom content manifests must pass schema and size validation before indexing.
- The server cannot generate, change, build, or install project code.
## Troubleshooting
Problem
What to check
No project tools
Use local stdio and pass an absolute application path with --project.
Project configuration fails
Confirm the application has a compatible project-local @tavojs/cli package and can run its tavo machine commands.
Guidance looks stale
Read tavo://status and compare the content hash and package compatibility report.
A response is truncated
Follow its continuation resource or request a larger maxTokens value.
## Resources
- [@tavojs/mcp on npm](https://www.npmjs.com/package/@tavojs/mcp)
- [Tavo.js MCP source](https://github.com/tavojs/tavo-mcp)
- [Model Context Protocol specification](https://modelcontextprotocol.io/specification/latest)
- [Tavo.js Framework documentation](/docs/core)
- [Tavo.js UI documentation](/docs/ui)
# Tavo.js Framework documentation
> The complete source of truth for building, rendering, extending, securing, testing, and operating a Tavo.js application.
Canonical page: https://tavojs.dev/docs/core
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: concept
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- Familiarity with TypeScript, TSX, and browser application development.
## Outcomes
- Explain how Tavo.js, its TSX runtime, Vite, file routes, loaders, rendering, and hydration fit together.
- Choose the correct Framework guide or exact API reference for a concrete development task.
## Choose the documentation flow
[
## Learn by building
Build your first Tavo.js application as one cumulative project. Use this when you are new to the Framework.Read guide →
](/docs/getting-started/first-app)[
## Understand a subsystem
Use the Framework guides for mental models, complete workflows, defaults, lifecycle, cleanup, security, and production behavior.Read guide →
](/docs/core/pages-and-layouts)[
## Look up an exact API
Use the generated reference for canonical imports, TypeScript signatures, runtime boundaries, stability, and owning guides.Read guide →
](/docs/core/api)
## How a Tavo.js application fits together
Tavo.js is a TypeScript and TSX framework with its own component runtime; it is not a React wrapper. Vite supplies the development server and bundling pipeline, while Tavo.js adds file routes, request data, server rendering, hydration, state, and production output.
Files in src/pages define the route tree. Middleware decides whether resolution continues, loaders prepare route data, and layouts compose the visible shell. Plain TSX components render interface; createTavo adds component-local state and controller behavior; stores share client state between consumers.
## Follow one URL through Tavo.js
**Reference Reference snippet**
```text
URL
→ match a file route
→ run global, layout, and page middleware
→ load layout data from root to leaf, then page data
→ resolve route metadata
→ render or reuse SSR/static output, optionally stream deferred sections
→ send HTML and serialized route state
→ hydrate the same tree in the browser
→ handle later client navigation or server actions
```
The exact work depends on the route mode. A CSR route sends a server shell and resolves in the browser. An SSR route renders for the request. Static and revalidated routes reuse SSR output according to their cache policy.
**Keep request state inside the request**
Loader results, sessions, users, and deferred work belong to one route resolution. Module variables, global stores, and runtime plugin capabilities may be shared by concurrent SSR requests.
## Browse the Framework by system
[
## Fundamentals
Application configuration, environment boundaries, TSX components, DOM behavior, and the public package map.Read guide →
](/docs/core/configuration)[
## Routing and data
File routes, layouts, navigation, loaders, middleware, actions, forms, cancellation, pending UI, and route failures.Read guide →
](/docs/core/pages-and-layouts)[
## Application model
createTavo, controllers, Stores, services, resources, async ownership, and shared application state.Read guide →
](/docs/core/mvc)[
## Rendering
SSR, CSR, hydration, streaming, cache policy, metadata, styling, images, fonts, scripts, and localization.Read guide →
](/docs/core/ssr-and-hydration)[
## Server and security
Node request handling, sessions, origins, trusted hosts, CSP, hydration exposure, remote assets, and failure containment.Read guide →
](/docs/core/security)[
## Extensions
Plugin API v1, manifests, permissions, capabilities, installation instances, lifecycle phases, collisions, and inspection.Read guide →
](/docs/core/plugins)[
## Quality and tooling
Testing, validation, diagnostics, scheduling, instrumentation, devtools, and development inspection.Read guide →
](/docs/core/testing-and-diagnostics)[
## Production
Build artifacts, static and Node delivery, monitoring, verification, and production troubleshooting.Read guide →
](/docs/core/deployment)[
## Reference
Route-module, configuration, runtime, diagnostic, stability, and canonical package API contracts.Read guide →
](/docs/core/api)
## How to read this reference
Each focused guide explains ownership and lifecycle, provides a complete example, states defaults and failure behavior, and ends with a way to verify the result. Reference pages describe the exact public contract; the generated API inventory mirrors the declarations in the connected Framework checkout.
**Public imports are the compatibility boundary**
Import only from @tavojs/core, /router, /server, /config, /plugin, or the experimental /dev entry point. The /server-only and JSX runtime paths exist for their technical boundaries. Source implementation paths are not public APIs.
## Use the production loop early
Development mode optimizes iteration. A production build verifies route discovery, generated types, SSR output, static generation, and bundle boundaries.
**Run Terminal**
```bash
npx tavo check
npx tavo build --report-json
npx tavo preview --ssr
```
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Application configuration
> Understand what belongs in tavo.config.ts, what belongs in vite.config.ts, and how to extend either file without discarding existing behavior.
Canonical page: https://tavojs.dev/docs/core/configuration
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project created by the current CLI.
- Node.js 20.19+ or 22.12+ with project dependencies installed.
## Outcomes
- Place Framework and Vite settings in the correct file.
- Merge plugins and build settings without deleting generated configuration.
- Verify configuration with the local CLI and a production build.
## Start from the generated configuration
Use `Node.js 20.19+ or 22.12+` and run commands from the project root. A generated project already contains both configuration files. Edit those files in place instead of replacing them with an example from another project.
File
Read by
Responsibility
`tavo.config.ts`
Tavo.js
Routes, CSS entries, plugins, diagnostics, build policy, and SSR behavior.
`vite.config.ts`
Vite through Tavo.js
Vite server, resolve, dependency, and bundler settings while preserving Tavo.js's TSX and build plugins.
`tsconfig.json`
TypeScript
Typechecking and Tavo.js's automatic TSX runtime configuration.
## Configure framework behavior in tavo.config.ts
`defineConfig` preserves literal types and catches unsupported top-level fields. Paths are relative to the project root. Keep every existing plugin and CSS entry when adding another integration. `Tavo.js` has one framework configuration file at the project root; imported and computed values are evaluated consistently by development, build, inspection, and production.
**Merge tavo.config.ts — merge with the existing file**
```ts
import { defineConfig } from "@tavojs/core/config";
import { tavoUi } from "@tavojs/ui/plugin";
export default defineConfig({
pagesDir: "src/pages",
cssEntries: ["src/styles.css"],
diagnostics: {
devOverlay: true,
traces: false
},
build: {
prerenderStyles: "inline",
budgets: {
firstLoadJs: "150kb",
routeJs: "40kb"
}
},
ssr: {
trustedHosts: ["example.com"],
canonicalOrigin: "https://example.com"
},
plugins: [tavoUi()]
});
```
**Route modules have a separate contract**
Rendering mode, loaders, metadata, static output, and revalidation belong to page and layout modules. Do not move route-specific exports into tavo.config.ts.
**Keep configuration in one file**
Export defineConfig as the default from root tavo.config.ts. Plugins stay at the top level and server options stay inside the nested ssr object. Configuration load errors are reported instead of silently ignored.
## Top-level TavoConfig reference
These are all accepted top-level `TavoConfig` properties in `tavo.config.ts`. `defineConfig` rejects any other top-level key at typecheck time. Every property is optional, but the file and its default `defineConfig` export are required.
**Reference tavo.config.ts**
```ts
import { defineConfig } from "@tavojs/core/config";
export default defineConfig({
pagesDir: "src/pages",
cssEntries: ["src/styles/app.scss"],
diagnostics: {
devOverlay: true,
traces: false
},
ssr: {
canonicalOrigin: "https://example.com"
}
});
```
Property
Accepted value
Default
What it changes
`pagesDir`
`string`
src/pages
Sets the project-root-relative directory scanned for pages, layouts, route groups, and special route modules.
`cssEntries`
`string[]`
Existing files among src/styles.css, src/styles.scss, src/app.css, and src/app.scss
Sets project-root-relative global CSS or Sass entries loaded by development, client build, and SSR.
`plugins`
[`PluginUse`](/docs/core/api/plugin#api-tavojs-core-plugin--pluginuse)`[``]` `|` `{` `use``:` [`PluginUse`](/docs/core/api/plugin#api-tavojs-core-plugin--pluginuse)`[``]``;` `overrides``?``:` [`PluginOverride`](/docs/core/api/plugin#api-tavojs-core-plugin--pluginoverride)`[``]` `}`
No app plugins
Installs Plugin API v1 integrations. Use the array form for normal installs and the object form only when public overrides are needed.
`diagnostics`
`{ devOverlay?: boolean; traces?: boolean }`
Omitted; generated apps set { devOverlay: true, traces: false }
Sets development error-overlay and framework trace preferences.
`build`
`{ prerenderStyles?: "inline" | "external"; budgets?: { … } }`
{ prerenderStyles: "inline", budgets: {} }
Controls prerendered CSS delivery and JavaScript budget enforcement.
`ssr`
`Tavo.js SSR options`
Runtime defaults shown below
Configures page-runtime behavior, Node requests, response caching, the HTML document, and image optimization.
**Paths are project-root relative**
pagesDir, cssEntries, ssr.images.publicDir, and the root used by normal build commands resolve from the application root. A configured CSS entry that does not exist is skipped by the production build; npx tavo check reports missing entries before release.
## Diagnostics and build properties
Property
Accepted value
Default
What it changes
`diagnostics.devOverlay`
`boolean`
Generated apps: true
Enables or disables the development error-overlay preference.
`diagnostics.traces`
`boolean`
Generated apps: false
Enables or disables detailed framework diagnostic traces.
`build.prerenderStyles`
`"inline" | "external"`
inline
Inlines collected route styles into prerendered HTML or writes references to external build assets.
`build.budgets.firstLoadJs`
`number | byte-size string`
No limit
Fails the production build when JavaScript needed for a route's first load exceeds this number of bytes.
`build.budgets.routeJs`
`number | byte-size string`
No limit
Fails the production build when JavaScript attributed to one route exceeds this number of bytes.
Budget strings accept bytes or the case-insensitive units b, kb, kib, mb, and mib; decimal strings such as 1.5mb are valid. Numeric values are bytes. CLI flags override the file for one build.
**Run Terminal**
```bash
npx tavo build --max-first-load-js 150kb --max-route-js 40kb
npx tavo build --prerender-styles external
```
## SSR and page-runtime properties
The ssr object accepts the following properties. The CLI supplies discovered route modules and compiles top-level plugins, so most applications configure only origin security, rendering, caches, or images here.
Property
Accepted value
Default
What it changes
`ssr.canonicalOrigin`
`absolute HTTP(S) origin string`
Origin derived from the request Host over HTTP
Sets the public origin behind TLS termination. Credentials, paths, queries, and fragments are rejected; its host is also trusted for actions.
`ssr.trustedHosts`
`string[]`
Localhost variants, plus canonicalOrigin when configured
Allows Host values used to validate Node action and plugin mutation requests. Entries may include a hostname or host with port.
`ssr.allowExternalRedirects`
`boolean`
false
Allows route and middleware redirects to absolute HTTP(S) URLs. Relative same-origin paths remain allowed without it.
`ssr.stream`
`boolean`
false
Streams the SSR response instead of buffering the completed document.
`ssr.maxRequestBodyBytes`
`number`
10 MiB
Limits buffered non-GET request bodies in the Node handler. Requests above the limit receive 413.
`ssr.maxResolvedCacheEntries`
`finite non-negative number`
1,024
Limits the process-local route-resolution data cache. Values are floored; 0 disables reuse.
`ssr.staticCache`
[`SsrStaticCache`](#ssr-static-cache)
Process-local memory cache with 1,024 entries
Stores rendered responses for static and revalidated routes. Supply an adapter for shared or durable caching.
`ssr.document`
[`RenderDocumentOptions`](#render-document-options)
Document defaults shown below
Sets the shared HTML shell, attributes, serialized initial state, CSP nonce, and style registry.
`ssr.images`
[`ImageOptimizerOptions`](#image-optimizer-options)
Optimizer defaults shown below
Configures /\_tavo/image, local and remote sources, transform limits, formats, and memory caching.
`ssr.getPageProps`
`() => Record`
No extra props
Adds application-owned props to page components on each runtime render.
`ssr.notFound`
[`Component`](/docs/core/api/components-and-dom#api-tavojs-core--component)`<``{` `pathname``:` `string` `}``>`
Discovered src/pages/404 module
Overrides the application-wide not-found component supplied by file routing.
`ssr.csrFallback`
[`Child`](/docs/core/api/components-and-dom#api-tavojs-core--child) `|` `(``{` `pathname``,` `params` `}``)` `=>` [`Child`](/docs/core/api/components-and-dom#api-tavojs-core--child)
null
Renders fallback content when a CSR route is resolved by the browser before its route module is ready.
`ssr.csrActions`
[`CsrActionsOptions`](#csr-actions-options)
Disabled
Maps browser form submissions for static CSR delivery to an action backend. Nested properties are listed below.
`ssr.middleware`
[`PageMiddleware`](/docs/core/api/router#api-tavojs-core-router--pagemiddleware)`[``]`
\[\]
Runs application-wide middleware around every route in addition to discovered layout and page middleware.
`ssr.i18n`
[`I18nService`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--i18nservice)
Registered default i18n service, otherwise none
Supplies locale detection, localized path resolution, and locale state to the pages runtime.
`ssr.instrumentation`
[`{ emit(event: TavoInstrumentationEvent): void }`](/docs/core/scheduling-and-instrumentation#event-contract)
None
Receives route resolve, middleware, loader, action, and cache lifecycle events. Listener failures do not interrupt framework work.
`ssr.modules`
`PageModules`
Generated route-module map
Overrides file-discovered modules. This is an advanced manual-runtime hook; normal applications leave it unset.
**Plugins do not belong under ssr**
ssr.plugins is intentionally not part of TavoConfig. Install plugins with the top-level plugins array or the public { use, overrides } form. Plugin graph compilation and runtime construction are framework host responsibilities.
## CSR action properties
Configure ssr.csrActions when a statically hosted CSR application should submit Tavo.js forms to a separate action server. resolveUrl takes precedence over baseUrl.
Property
Accepted value
Default
What it changes
`ssr.csrActions.enabled`
`boolean`
false
Intercepts eligible non-GET forms and enables action URL mapping.
`ssr.csrActions.baseUrl`
`string`
Current origin and route path
Resolves the route pathname and query against a separate action-server base URL.
`ssr.csrActions.resolveUrl`
`({ pathname, search, form? }) => string`
Uses baseUrl, then the route path
Computes the complete action request URL for each route or form.
`ssr.csrActions.credentials`
`RequestCredentials`
include
Sets the Fetch credentials mode used for intercepted form submissions.
`ssr.csrActions.headers`
`HeadersInit | ({ pathname, form }) => HeadersInit`
No additional headers
Adds fixed or per-form request headers to the action fetch.
**Reference tavo.config.ts — send static-site forms to an action service**
```ts
import { defineConfig } from "@tavojs/core/config";
export default defineConfig({
ssr: {
csrActions: {
enabled: true,
baseUrl: "https://actions.example.com",
credentials: "include",
headers: ({ pathname }) => ({
"X-Tavo.js-Route": pathname
})
}
}
});
```
## HTML document properties
ssr.document sets defaults for the server-rendered document shell. Route head exports and Tavo.js SEO components can refine metadata per route.
Property
Accepted value
Default
What it changes
`ssr.document.lang`
`string`
en
Sets the escaped html lang attribute.
`ssr.document.title`
`string`
No title
Sets the escaped fallback document title. Route SEO metadata takes precedence.
`ssr.document.unsafeHeadHtml`
`string`
Empty
Appends trusted raw HTML to head. It is not escaped; prefer TSX metadata for all structured head content.
`ssr.document.htmlAttributes`
`Record`
{}
Adds escaped safe-name attributes to html. false omits an attribute and true renders a boolean attribute.
`ssr.document.bodyAttributes`
`Record`
{}
Adds escaped attributes to body.
`ssr.document.appAttributes`
`Record`
{}
Adds escaped attributes to the application container.
`ssr.document.doctype`
`string`
Sets the exact doctype prefix written before the html element.
`ssr.document.appContainerId`
`string`
app
Sets the escaped id of the application container used by hydration.
`ssr.document.initialState`
`unknown`
Omitted
Serializes JSON into a protected application/json script after the app container.
`ssr.document.stateScriptId`
`string`
\_\_TAVO\_STATE\_\_
Sets the escaped id of the serialized initial-state script.
`ssr.document.nonce`
`string`
None
Adds a CSP nonce to generated state and style elements.
`ssr.document.beforeRender`
`() => void`
None
Runs immediately before server rendering. Use only for request-safe setup.
`ssr.document.styleRegistry`
[`StyleRegistry`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--styleregistry)
A new registry for each render
Supplies a custom style collector with add, has, and entries methods.
**Raw HTML is always explicit**
There is no head string alias. Use escaped TSX metadata first, and use unsafeHeadHtml only when an integration truly requires trusted raw markup.
## Image optimizer properties
Property
Accepted value
Default
What it changes
`ssr.images.enabled`
`boolean`
true
Enables the /\_tavo/image optimization endpoint.
`ssr.images.allowRemote`
`boolean`
false
Allows remote HTTP(S) image sources, still subject to remotePatterns and network safety checks.
`ssr.images.remotePatterns`
`Array`
\[\]
Allowlists remote origins or host/path patterns. A hostname beginning with \*. matches subdomains only.
`ssr.images.publicDir`
`string`
public
Sets the project-root-relative source directory for local images.
`ssr.images.quality`
`number`
75
Sets fallback output quality; request values are clamped from 1 through 100.
`ssr.images.cacheMaxAge`
`number`
31,536,000 seconds
Sets the Cache-Control max-age of transformed image responses.
`ssr.images.defaultFormat`
`"webp" | "avif" | "jpeg" | "png" | "original"`
webp
Sets the output format when the optimization URL does not request one.
`ssr.images.sizes`
`number[]`
\[320, 640, 960, 1280, 1600\]
Sets the candidate widths and supplies the fallback width for an optimization request.
`ssr.images.timeoutMs`
`number`
5,000
Limits remote fetch duration.
`ssr.images.maxBytes`
`number`
10 MiB
Rejects local or remote source images larger than this byte limit.
`ssr.images.memoryCacheMaxEntries`
`number`
128
Limits transformed images stored in process memory. Values are floored; 0 disables storage.
`ssr.images.maxConcurrentTransforms`
`number`
4
Limits simultaneous Sharp transforms; values are floored with a minimum of 1.
`ssr.images.maxPendingTransforms`
`number`
64
Limits queued transforms before the optimizer returns a busy error.
`ssr.images.allowInsecureRemote`
`boolean`
false
Allows HTTP remote image URLs. Keep disabled unless the transport risk is explicitly accepted.
`ssr.images.resolveHostname`
`(hostname: string) => Promise>`
Node DNS lookup
Overrides DNS resolution used by private-network protection. Intended for adapters and controlled tests.
**Reference tavo.config.ts — allow a narrow remote image source**
```ts
import { defineConfig } from "@tavojs/core/config";
export default defineConfig({
ssr: {
images: {
allowRemote: true,
remotePatterns: [
{
protocol: "https:",
hostname: "images.example.com",
pathname: "/products"
}
],
defaultFormat: "avif",
quality: 80
}
}
});
```
Server image transforms require the optional sharp dependency in the application that runs SSR. Remote requests are checked against the allowlist, redirects, DNS results, private-network addresses, timeout, and byte limit.
## SsrStaticCache adapter
Use the public SsrStaticCache interface from @tavojs/core/server when rendered static and revalidated responses must be shared across processes or persisted outside Node memory. Methods may return values directly or through promises.
**Reference src/server/static-cache.ts — minimal adapter**
```ts
import type {
SsrStaticCache,
SsrStaticCacheEntry
} from "@tavojs/core/server";
const entries = new Map();
export const staticCache: SsrStaticCache = {
get(key) {
return entries.get(key) ?? null;
},
set(key, entry) {
entries.set(key, entry);
},
delete(key) {
entries.delete(key);
},
invalidateTags(tags) {
const requested = new Set(tags);
let deleted = 0;
for (const [key, entry] of entries) {
if (entry.tags.some((tag) => requested.has(tag))) {
entries.delete(key);
deleted += 1;
}
}
return deleted;
},
clear() {
entries.clear();
}
};
```
`SsrStaticCacheEntry` contains the rendered response, an absolute `expiresAt` timestamp or null, and its cache tags. The built-in `createMemoryStaticCache` is available from `@tavojs/core/server`. The Map above illustrates the contract; use a shared cache implementation for multi-process production deployments.
**Tag invalidation is optional but operationally important**
Without invalidateTags, cache-tag invalidation returns zero and cannot remove matching entries. Maintain a tag index in production adapters when invalidation volume makes a full scan inappropriate.
## Configuration loading behavior
**Reference scripts/read-config.ts**
```ts
import { loadTavoConfig } from "@tavojs/core/dev";
const config = await loadTavoConfig(process.cwd(), {
mode: "development"
});
console.log(config.pagesDir);
```
Concern
Contract
Location
Exactly one tavo.config.ts at the project root.
Export
A default export returned by defineConfig({ … }). A plain object is rejected.
Evaluation
Imported and computed values are supported. The file is evaluated once per project root and process.
Environment
.env files are loaded before evaluation. The explicit mode wins, then NODE\_ENV, then production.
Mode safety
One project root cannot be reevaluated in a different mode in the same process.
Failure
A failed load is not cached, so the next command or retry can load a corrected file.
## Keep Tavo.js's Vite wrapper
defineTavoViteConfig installs Tavo.js's TSX transform, file-route build guards, SVG support, localization splitting, and plugin build contributions. Pass your Vite settings into the wrapper rather than replacing it with Vite's defineConfig.
**Replace vite.config.ts — replace only the empty wrapper call**
```ts
import { defineTavoViteConfig } from "@tavojs/core/config";
export default defineTavoViteConfig(({ mode }) => ({
server: {
port: mode === "development" ? 4174 : undefined
},
build: {
sourcemap: mode !== "production"
}
}));
```
## Verify the result
**Run Terminal**
```bash
npx tavo info
npx tavo check
npm run build
```
info shows the resolved pages and CSS configuration, check reports route and project-shape problems, and the production build exercises both client and server configuration. If a plugin was added, run `npx tavo inspect plugins` as well.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Environment variables
> Load server configuration safely, expose only deliberate public values to browser code, and understand mode-specific .env files.
Canonical page: https://tavojs.dev/docs/core/environment-variables
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with server rendering or build-time configuration.
## Outcomes
- Choose a server, browser, or build boundary for each environment value.
- Understand Tavo.js's .env file precedence.
- Keep secrets behind an enforced server-only module boundary.
## Choose the boundary before naming the variable
Boundary
Example
Read it from
Server only
`process.env.DATABASE_URL`
src/server, a server loader, action, middleware, or server plugin phase
Browser safe
`import.meta.env.VITE_PUBLIC_API_ORIGIN`
A normal source module used by browser code
Build/configuration
`process.env.TAVO_SITE_URL`
tavo.config.ts or server-side build tooling
**VITE\_ means public**
Vite replaces VITE\_-prefixed values in browser bundles. Never use that prefix for passwords, signing keys, database URLs, private tokens, or any value that must remain secret.
## Use mode-specific files deliberately
Tavo.js's server runtime reads these files from the project root. Later files override earlier files, while an environment variable already supplied by the shell or hosting platform wins over every file.
**Reference Environment load order**
```text
.env
.env.local
.env.development
.env.development.local
# A production run uses .env.production and .env.production.local instead.
```
Commit non-secret defaults in .env when appropriate. Keep .env.local and mode-local files out of source control. Commit an .env.example containing names and safe placeholders so new developers know what the application requires.
## Read secrets behind a server-only boundary
**Create src/server/projects.ts — create this server-only module**
```ts
import "@tavojs/core/server-only";
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error("DATABASE_URL is required");
}
export function getProjectsDatabaseUrl(): string {
return databaseUrl;
}
```
Files under src/server and modules importing the server-only marker are framework-enforced client-build boundaries. Import them from a server loader, action, middleware, or server plugin phase. Do not statically import them into a component or universal loader.
## Expose only browser-safe configuration
**Run .env.example — add the required public name**
```bash
VITE_PUBLIC_API_ORIGIN=https://api.example.com
```
**Create src/config/public.ts — create this shared module**
```ts
export const publicConfig = {
apiOrigin: import.meta.env.VITE_PUBLIC_API_ORIGIN
};
```
Restart the development server after changing an environment file. Verify a server value through the route or handler that consumes it; verify a public value in the browser without printing unrelated environment data.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Components and JSX
> Build functional Tavo.js components, understand the JSX contract, and choose when local behavior belongs in createTavo.
Canonical page: https://tavojs.dev/docs/core/components-and-jsx
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application configured for the automatic JSX runtime.
- Familiarity with TypeScript functions and HTML attributes.
## Outcomes
- Write plain functional components with typed props and children.
- Use intrinsic attributes, events, class names, and controlled values correctly.
- Choose a function component, createTavo model, controller, or shared Store deliberately.
## Start with a function component
A Tavo.js component is a function that receives props and returns a renderable [`Child`](/docs/core/api/components-and-dom#api-tavojs-core--child "View Child in the Core API reference"). Use a plain function when output depends only on props, children, or application services that are already reactive.
The automatic JSX runtime compiles TSX for you. Import runtime values such as [`Fragment`](/docs/core/api/components-and-dom#api-tavojs-core--fragment "View Fragment in the Core API reference") only when you reference them explicitly.
- Strings, numbers, elements, nested child arrays, null, undefined, and booleans are valid children. Nullish and boolean children do not produce HTML.
- [`Fragment`](/docs/core/api/components-and-dom#api-tavojs-core--fragment "View Fragment in the Core API reference") groups siblings without adding a DOM element.
- Explicit children passed between component tags are available through `props.children`.
- A `className` may be one string or an array of strings; arrays are joined with spaces.
- A style value may be a CSS string or an object. Object keys written in camel case become kebab-case CSS properties during SSR.
**Reference src/components/StatusBadge.tsx**
```tsx
import type { PropsWithChildren } from "@tavojs/core";
type StatusBadgeProps = PropsWithChildren<{
tone: "neutral" | "success" | "danger";
}>;
export function StatusBadge({
tone,
children,
}: StatusBadgeProps) {
return (
{children}
);
}
```
## Choose the smallest state owner
Rendering and state ownership are separate decisions. Keep a component functional until it needs local reactive state, behavior, lifecycle, or cleanup.
- Use a plain function component for render-only output.
- Use [`createTavo`](/docs/core/api/application#api-tavojs-core--createtavo "View createTavo in the Core API reference") with a model for state owned by one mounted component.
- Add a [`TavoController`](/docs/core/api/application#api-tavojs-core--tavocontroller "View TavoController in the Core API reference") for behavior, routing, services, refs, async actions, or managed side effects.
- Use a global [`Store`](/docs/core/api/application#api-tavojs-core--store "View Store in the Core API reference") for browser state intentionally shared by multiple routes or component owners.
- Use a route loader for request-specific and route-critical data; do not place request identity in global state.
**Reference src/components/NameField.tsx**
```tsx
import { createTavo, TavoController } from "@tavojs/core";
type NameFieldState = {
name: string;
};
class NameFieldController extends TavoController {
updateName(event: Event) {
const input = event.currentTarget as HTMLInputElement;
this.model.patch({ name: input.value });
}
}
export const NameField = createTavo<
Record,
NameFieldState,
NameFieldController
>({
model() {
return { name: "" };
},
controller: NameFieldController,
view({ state, controller }) {
return (
);
},
});
```
## Understand intrinsic element behavior
- Text and attribute values are escaped. Unsafe attribute names and javascript-style URL protocols are rejected.
- Event props begin with on and are attached by the browser runtime; they are not serialized into server HTML.
- ref, use, transition, and key are runtime instructions rather than HTML attributes.
- Boolean true emits a boolean attribute; false, null, and undefined omit the attribute.
- HTML void elements such as input, img, and br render without closing tags.
- Unknown safe intrinsic attributes pass through, which keeps data-\* and aria-\* attributes available.
**Reference src/components/ProfileLink.tsx**
```tsx
export function ProfileLink({
active,
userId,
}: {
active: boolean;
userId: string;
}) {
return (
View profile
);
}
```
**Render purity keeps hydration predictable**
A component may render on the server and again in the browser. Do not read window, document, time, randomness, or browser storage while producing initial output. Move browser work to a controller lifecycle hook.
## Mount outside the Pages runtime
Most applications let Auto Pages create and hydrate the root. Use [`createRoot`](/docs/core/api/components-and-dom#api-tavojs-core--createroot "View createRoot in the Core API reference") when Tavo.js is embedded in an existing page, widget host, test shell, or other manually owned DOM container.
- `root.render`(node) owns repeat renders; `root.unmount`() removes the tree and releases refs, directives, listeners, and controller cleanup.
- Use `root.hydrate`(node) only when the container already holds matching server-rendered `Tavo.js` markup.
- render(node, container) is the convenience form for a one-off browser mount when you do not need the [`Root`](/docs/core/api/components-and-dom#api-tavojs-core--root "View Root in the Core API reference") handle.
- `renderToString`(node) returns escaped static HTML. Use the server rendering APIs instead when you need a complete document, route resolution, status, headers, head output, streaming, or hydration state.
- The automatic JSX transform normally creates [`VNode`](/docs/core/api/components-and-dom#api-tavojs-core--vnode "View VNode in the Core API reference") values. [`h`](/docs/core/api/components-and-dom#api-tavojs-core--h "View h in the Core API reference") is the lower-level explicit constructor for tooling or non-JSX integrations.
**Reference src/widget.tsx**
```tsx
import { createRoot } from "@tavojs/core";
import { SupportWidget } from "./SupportWidget";
const container = document.querySelector("#support-widget");
if (!(container instanceof HTMLElement)) {
throw new Error("Missing #support-widget container.");
}
const root = createRoot(container);
root.render();
// Call this when the host removes the widget.
export function unmountSupportWidget() {
root.unmount();
}
```
**Reference src/render-card.tsx**
```tsx
import { renderToString } from "@tavojs/core";
import { ReceiptCard } from "./ReceiptCard";
export function renderReceiptCard(total: string) {
return renderToString();
}
```
**Do not create a second application root**
Inside an Auto Pages application, render normal components from the route tree. A manual root is for a separately owned DOM container, not for replacing the route runtime.
## Treat value and checked as controlled
When value or checked comes from model state, update that same state from the corresponding event. The browser runtime restores the rendered value after an input event, so a static controlled value intentionally remains pinned.
- Use `onChange` for live text-entry updates; `Tavo.js` maps the browser input event into the controlled-field flow.
- Read `event.currentTarget` or `event.target` as the correct input element before updating the model.
- Give every field an accessible label and expose validation with normal HTML constraints and `aria-describedby` where needed.
- Use [`focusFirstInvalid`](/docs/core/api/components-and-dom#api-tavojs-core--focusfirstinvalid "View focusFirstInvalid in the Core API reference") after a failed client validation pass when moving focus is helpful and expected.
## Verify both render environments
- Render the component to HTML and confirm text, escaping, boolean attributes, class names, styles, and void elements.
- Mount it in a browser test and confirm event updates, controlled values, focus, and cleanup.
- Hydrate server markup and check that the first browser output matches without a hydration diagnostic.
- Typecheck public props and event targets; do not rely on casts hidden inside application callers.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# DOM and accessibility
> Own DOM handles safely with refs, directives, focus utilities, transitions, observers, and explicit cleanup.
Canonical page: https://tavojs.dev/docs/core/dom-and-accessibility
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A client-rendered or hydrated Tavo.js component.
- A DOM behavior that cannot be expressed through declarative props alone.
## Outcomes
- Use object, callback, merged, and keyed-list refs.
- Attach reusable directives and understand their cleanup timing.
- Build accessible focus ownership and managed observation.
## Use refs for direct DOM ownership
[`createRef`](/docs/core/api/components-and-dom#api-tavojs-core--createref "View createRef in the Core API reference") returns a mutable object whose current value follows one intrinsic element. Tavo.js assigns it on mount and hydration, moves it when the backing node changes, and clears it on replacement or unmount.
- Object refs are convenient controller fields. Callback refs receive the node and later receive null during cleanup.
- [`mergeRefs`](/docs/core/api/components-and-dom#api-tavojs-core--mergerefs "View mergeRefs in the Core API reference") combines multiple ref owners into one callback ref.
- [`createListRefs`](/docs/core/api/components-and-dom#api-tavojs-core--createlistrefs "View createListRefs in the Core API reference") creates stable object refs by a string or numeric key; delete removed keys and clear the collection when its owner is disposed.
- [`setRef`](/docs/core/api/components-and-dom#api-tavojs-core--setref "View setRef in the Core API reference") is useful when composing a higher-level component or adapter that forwards a DOM node.
- Refs are never serialized into server HTML and remain null during server rendering.
**Reference src/components/SearchField.tsx**
```tsx
import {
createRef,
createTavo,
TavoController,
} from "@tavojs/core";
class SearchFieldController extends TavoController {
input = createRef();
onMount() {
this.input.current?.select();
}
}
export const SearchField = createTavo({
controller: SearchFieldController,
view({ controller }) {
return (
);
},
});
```
**Reference src/components/ResultList.tsx**
```tsx
import {
createListRefs,
createRef,
createTavo,
mergeRefs,
TavoController,
} from "@tavojs/core";
type Result = { id: string; label: string };
class ResultListController extends TavoController {
items = createListRefs();
measuredItem = createRef();
featuredItem = mergeRefs(
this.items.get("featured"),
this.measuredItem,
);
onUnmount() {
this.items.clear();
}
}
export const ResultList = createTavo<
{ results: Result[] },
Record,
ResultListController
>({
controller: ResultListController,
view({ props, controller }) {
return (
{props.results.map((result) => (
{result.label}
))}
);
},
});
```
## Attach reusable behavior with directives
An [`ElementDirective`](/docs/core/api/components-and-dom#api-tavojs-core--elementdirective "View ElementDirective in the Core API reference") receives an HTMLElement after it mounts and may return cleanup. Pass one directive or an array through the intrinsic use prop. When the directive value changes, Tavo.js cleans up the old value before applying the new one; unmount also runs cleanup.
- [`autoFocus`](/docs/core/api/components-and-dom#api-tavojs-core--autofocus "View autoFocus in the Core API reference") queues focus after mount and accepts normal FocusOptions.
- [`transition`](/docs/core/api/components-and-dom#api-tavojs-core--transition "View transition in the Core API reference") applies enter immediately, enterActive in a microtask, and leave classes/callbacks during cleanup.
- transition does not wait for a CSS duration before removing a node. Use it for state classes and callbacks, not as an exit-animation coordinator.
- Create reusable behavior with `createDirective` and attach it through the intrinsic use prop. Use `setRef` when a component or adapter must forward a DOM node to another ref owner.
**Reference src/components/LiveNotice.tsx**
```tsx
import {
autoFocus,
createDirective,
transition,
} from "@tavojs/core";
const announce = createDirective((element) => {
element.setAttribute("aria-live", "polite");
return () => {
element.removeAttribute("aria-live");
};
});
const focusNotice = autoFocus();
const revealNotice = transition({
classes: {
enter: "notice--enter",
enterActive: "notice--visible",
leave: "notice--leave",
},
});
export function LiveNotice({ message }: { message: string }) {
return (
{message}
);
}
```
## Give dialogs explicit focus ownership
Accessible overlays need an initial focus target, contained Tab navigation, and restoration when the overlay closes. Keep every listener and restoration function under the same component owner.
- [`getFocusableElements`](/docs/core/api/components-and-dom#api-tavojs-core--getfocusableelements "View getFocusableElements in the Core API reference") returns visible links, buttons, enabled form controls, and eligible tabindex elements in DOM order.
- [`focusFirst`](/docs/core/api/components-and-dom#api-tavojs-core--focusfirst "View focusFirst in the Core API reference") returns the focused element or null. [`focusFirstInvalid`](/docs/core/api/components-and-dom#api-tavojs-core--focusfirstinvalid "View focusFirstInvalid in the Core API reference") targets the first control matching :invalid.
- [`trapFocus`](/docs/core/api/components-and-dom#api-tavojs-core--trapfocus "View trapFocus in the Core API reference") returns the keydown-listener cleanup. If no child is focusable, the container itself must be focusable.
- Focus trapping alone does not provide a complete modal: also label the dialog, prevent background interaction, support Escape where appropriate, and restore focus.
**Reference src/components/Dialog.tsx**
```tsx
import {
captureFocusRestore,
createRef,
createTavo,
focusFirst,
TavoController,
trapFocus,
} from "@tavojs/core";
import type { PropsWithChildren } from "@tavojs/core";
class DialogController extends TavoController {
dialog = createRef();
onMount() {
const restoreFocus = captureFocusRestore();
const dialog = this.dialog.current;
if (!dialog) {
return restoreFocus;
}
focusFirst(dialog);
const stopTrap = trapFocus(dialog);
return () => {
stopTrap();
restoreFocus();
};
}
}
export const Dialog = createTavo<
PropsWithChildren<{ label: string }>,
Record,
DialogController
>({
controller: DialogController,
view({ props, controller }) {
return (
{props.children}
);
},
});
```
## Observe elements with a managed owner
The standalone observer helpers [`observeResize`](/docs/core/api/components-and-dom#api-tavojs-core--observeresize "View observeResize in the Core API reference"), [`observeIntersection`](/docs/core/api/components-and-dom#api-tavojs-core--observeintersection "View observeIntersection in the Core API reference"), and [`observeMutation`](/docs/core/api/components-and-dom#api-tavojs-core--observemutation "View observeMutation in the Core API reference") accept an element or ref and return a disconnect function. Inside a [`TavoController`](/docs/core/api/application#api-tavojs-core--tavocontroller "View TavoController in the Core API reference"), the matching methods automatically register that disconnect function for component teardown.
- `observeResize` and `observeIntersection` accept an Element or `DomRefObject`.
- `observeMutation` accepts a Node or a ref object.
- Browser support failures surface from the platform constructors; add a feature check or polyfill when supporting older environments.
- Do not start observers during model creation, controller construction, or SSR.
**Reference Reference snippet**
```tsx
class ChartController extends TavoController {
chart = createRef();
onMount() {
this.observeResize(this.chart, () => {
this.measureChart();
});
this.observeIntersection(this.chart, (entries) => {
this.model.patch({ visible: entries[0]?.isIntersecting ?? false });
});
}
measureChart() {
// Read the committed chart dimensions.
}
}
```
## Verify cleanup and keyboard behavior
- Mount, replace, and unmount the element; assert object refs and callback refs are cleared.
- Change a use prop and confirm the previous directive cleanup runs before the new directive.
- Tab forward and backward through a focus trap, test an empty trap, and confirm focus restoration.
- Unmount an observed component and assert the observer disconnects.
- Run the same component through SSR and confirm ref, use, transition, and event instructions do not become HTML attributes.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Pages and layouts
> Turn files into routes, compose nested application shells, and provide route-specific pending and error views.
Canonical page: https://tavojs.dev/docs/core/pages-and-layouts
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Turn files into routes, compose nested application shells, and provide route-specific pending and error views.
## Start with the route tree
`Tavo.js` derives application routes from src/pages. Keep route modules focused on route concerns: loader data, metadata, render mode, and the page component. Move reusable interface and business behavior into components, controllers, and stores.
Dynamic segments use brackets, catch-all segments use three dots, and folders wrapped in parentheses organize files without changing the public URL.
**Reference src/pages/ — example route tree**
```text
src/pages/index.tsx → /
src/pages/dashboard/index.tsx → /dashboard
src/pages/blog/[id].tsx → /blog/:id
src/pages/docs/[[...slug]].tsx → /docs/*?slug
src/pages/(marketing)/about.tsx → /about
```
## Define a functional page
Export route behavior as named functions and render the completed result from the default component. The filename determines the URL; `PageProps`, `PagePendingProps`, `PageErrorProps`, and `PageLoadContext` provide explicit data and parameter types without wrapping the module.
**Create src/pages/projects/[id].tsx — create this route module**
```tsx
import type {
PageErrorProps,
PageLoadContext,
PagePendingProps,
PageProps
} from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Project = { name: string };
type ProjectParams = { id: string };
export async function load({ params, signal, url }: PageLoadContext): Promise {
const endpoint = new URL(`/api/projects/${params.id}`, url);
const response = await fetch(endpoint, { signal });
if (!response.ok) throw new Error("Project could not be loaded");
return response.json();
}
export function pending({ params }: PagePendingProps) {
return Loading project {params.id}…;
}
export function error({ pathname }: PageErrorProps) {
return Could not load {pathname}.;
}
export default function ProjectPage({ data }: PageProps) {
return {data?.name};
}
```
## Compose layouts and failures
A `_layout.tsx` file wraps every descendant route. Layouts compose from root to leaf, so the root can own global navigation while a dashboard layout owns its sidebar and shared loader data. Each layout receives its own loader result as data and its own loader failure as error.
- Use route groups for alternate shells without adding a URL segment.
- Add `src/pages/404.tsx` for unmatched URLs.
- Export pending from a page for active client-navigation feedback after its layout loaders resolve.
- Export error from a page for its contextual loader-failure view; otherwise `src/pages/_error.tsx` is the application-wide fallback.
- A `notFound`() signal bypasses both error views and renders `src/pages/404.tsx`.
- Handle a layout-loader failure from that layout's error prop; descendant loaders continue unless the loader signals not found.
- Keep request-specific data in layout loader results, not process-wide stores during SSR.
## Page module reference
A page or layout is a functional default export with optional named route exports. The filename determines the public route, while load, action, middleware, head, render, prerender, revalidate, vary, `cacheTags`, and `generateStaticParams` add route behavior. Page modules can additionally export pending and error components.
Page and layout components receive the resolved URL params and their own loader data. The layers array preserves every layout and page result, while `layerData` exposes those results by layer ID.
**Reference Reference snippet**
```ts
type PageModuleRecord = {
default: Component>;
pending?: Component;
error?: Component;
load?: PageLoader;
action?: PageAction;
head?: PageHeadExport | ((context: PageLoadContext & {
data: unknown; error: unknown;
}) => PageHeadExport);
middleware?: PageMiddleware | PageMiddleware[];
render?: "csr";
prerender?: boolean;
static?: boolean;
revalidate?: number | false;
vary?: string | string[];
cacheTags?: string | string[] | ((context: PageLoadContext) => MaybePromise);
generateStaticParams?: () => MaybePromise[]>;
};
type PageProps> = {
pathname?: string;
params: TParams;
data?: TData;
error?: unknown;
layers?: Array<{ id: string; kind: "layout" | "page"; data: unknown; error: unknown }>;
layerData?: Record;
};
type PagePendingProps> = {
pathname: string;
params: TParams;
layers: RouteDataLayer[];
layerData: Record;
};
type PageErrorProps> =
PagePendingProps & {
data: unknown;
error: unknown;
};
```
## Route conventions and matching
- `index.tsx` maps to its folder path; `_layout.tsx` wraps descendants; `404.tsx` handles unmatched paths; a page error export handles its loader failure before the global `_error.tsx` fallback.
- \[id\] is a required dynamic segment, \[\[id\]\] is optional, \[...all\] is a required catch-all, and \[\[...all\]\] is an optional catch-all.
- Folders in parentheses are route groups: they organize files and select layouts without adding a URL segment.
- `RouteParamsFromPath` derives string parameters. Optional parameters are string | undefined. `LoaderData` unwraps the loader's awaited return type.
- [`defineRoutePage`](/docs/core/api/router#api-tavojs-core-router--defineroutepage "View defineRoutePage in the Core API reference") is optional route-aware typing assistance. It does not register or rename a route; keep its path literal aligned with the filename and confirm the result with tavo routes.
- `defineRoutePage` also infers route params for pending and error components when the helper form is useful.
- The CLI generates functional modules by default. Use `tavo` generate page `--typed-route` only when the helper form is useful.
- Routes are sorted by segment specificity before matching: static segments win over dynamic segments, required parameters win over optional ones, and catch-all segments come last. Equivalent patterns use a lexical path tie-break, so filesystem discovery order never changes the result.
**Reference Reference snippet**
```tsx
// src/pages/projects/[projectId]/tasks/[[taskId]].tsx
import type { PageProps } from "@tavojs/core/router";
type Params = {
projectId: string;
taskId?: string;
};
export default function TaskPage({ params }: PageProps) {
return (
Project {params.projectId}; task {params.taskId ?? "overview"}
);
}
```
**Inspect the generated route graph**
Route collisions, invalid files, CSR-incompatible static options, and dynamic head on CSR routes become manifest diagnostics. Run tavo routes and tavo check before relying on route order.
## Route pending and error reference
A page can export pending for unresolved browser navigation and error for a contextual page-loader failure. Both exports are normal `Tavo.js` components: use a function component for render-only feedback or `createTavo` when the state needs a model, controller, lifecycle, or cleanup.
- Client navigation changes the URL, runs middleware, resolves target layout loaders, renders pending inside those layouts, runs the page loader, and then renders the completed page or route error.
- The default page component and controller do not mount until the page loader completes. Pending props intentionally omit page loader data but include resolved layout layers and `layerData`.
- A `createTavo` pending component's controller receives the target route through `this.page`, including pathname, route, status, params, layers, and `layerData`; `this.page.data` is unavailable while the loader is unresolved.
- Without pending, the previous page stays visible while the target content region is marked busy.
- The target page error export wins for its loader failure; otherwise `Tavo.js` renders `src/pages/_error.tsx`. `notFound`() bypasses both and renders `src/pages/404.tsx` with status 404.
- A layout-loader failure enters error handling instead of rendering pending with invalid layout data.
- Normal SSR, static prerendering, prefetching, and fresh route-cache hits do not render pending.
- A replaced navigation aborts obsolete resolution, removes its pending view, and prevents stale data or errors from becoming active.
- Use `aria-busy` and an accessible label for pending UI, avoid moving focus into a skeleton, and announce route errors without exposing private diagnostic details.
**Reference Reference snippet**
```tsx
import type {
PageErrorProps,
PagePendingProps
} from "@tavojs/core/router";
export function pending({ params }: PagePendingProps<{ id: string }>) {
return Loading report {params.id}…;
}
export function error({ pathname }: PageErrorProps<{ id: string }>) {
return Could not load {pathname}.;
}
```
## Rendering, static output, and cache options
SSR is the normal server render mode. A route becomes CSR when its module chain selects render: "csr". Static and revalidation policy composes across root, layouts, and page rather than belonging only to the leaf page.
- In functional modules, export const prerender = true enables static output. false or revalidate = false disables an inherited static policy.
- revalidate is measured in seconds, rounded down, clamped to zero, and the shortest finite value in the module chain wins.
- vary header names are lowercased and deduplicated. Localization also varies cached output by Accept-Language.
- `cacheTags` can be static strings or request-aware resolvers. Tags support targeted invalidation in runtimes that expose it.
- CSR routes ignore static, revalidate, vary, cache tags, and `generateStaticParams`. The manifest reports incompatible declarations.
- `generateStaticParams` is required to enumerate build-time paths for a dynamic static route.
**Reference Reference snippet**
```tsx
import { defineRoutePage } from "@tavojs/core/router";
export default defineRoutePage("/catalog/[id]", {
static: true,
revalidate: 300,
vary: "accept-language",
cacheTags: ({ params }) => ["catalog", `product:${params.id}`],
generateStaticParams: async () => [{ id: "starter" }],
load: ({ params, signal }) => getProduct(params.id, { signal }),
default: ({ data }) =>
});
```
**Choose one static form**
Use named prerender in a functional module or static inside defineRoutePage. Declaring both forms in one route is rejected.
## Configure shared route behavior
- Put application-wide page props, not-found UI, CSR fallback content, middleware, localization, redirect policy, trusted hosts, and cache limits under ssr in `tavo.config.ts`.
- Configure `ssr.csrFallback` through `defineConfig`; normal applications use the framework boot flow and do not construct a pages runtime.
- Install plugins through the top-level plugins configuration. Plugin graph compilation and runtime construction are framework host responsibilities.
- Use `tavo` routes and `tavo` inspect route `--json` for route inspection. Experimental tooling can use the supported `@tavojs/core/dev` inspection exports.
**Reference Reference snippet**
```ts
// tavo.config.ts
import { defineConfig } from "@tavojs/core/config";
export default defineConfig({
ssr: {
csrFallback: "Loading application…",
maxResolvedCacheEntries: 512
}
});
```
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Route files and matching
> Use the exact src/pages conventions, understand deterministic route precedence, and place route-level failure UI correctly.
Canonical page: https://tavojs.dev/docs/core/route-files-and-matching
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: build, server, browser
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application with its route modules under src/pages.
- Familiarity with functional TSX components.
## Outcomes
- Map files and folders to public paths without route collisions.
- Predict which route wins when several patterns could match.
- Choose page error, global error, or 404 UI for each failure.
## Map src/pages to URLs
Tavo.js discovers route modules below src/pages. A normal page file contributes a URL; a special module changes how matching, layout, or failure rendering works. Route groups organize files and select layouts without adding a path segment.
**Reference src/pages — route map**
```text
src/pages/
index.tsx → /
about.tsx → /about
projects/
_layout.tsx → wraps project descendants
index.tsx → /projects
new.tsx → /projects/new
[id].tsx → /projects/:id
[[tab]].tsx → /projects/:?tab
[...path].tsx → /projects/*path
(account)/
_layout.tsx → selects a layout; no URL segment
settings.tsx → /settings
404.tsx → unmatched and notFound() UI
_error.tsx → fallback for page-loader errors
```
File syntax
What it matches
Result
`[id]`
Exactly one required segment.
params.id is a decoded string.
`[[tab]]`
Zero or one segment.
params.tab is string | undefined.
`[...path]`
One or more remaining segments.
params.path contains the decoded slash-joined value.
`[[...path]]`
Zero or more remaining segments.
params.path is string | undefined.
`(account)`
No URL segment.
The group remains part of layout identity.
**Malformed encoded paths do not match**
Route parameters are decoded before they are published. If a path segment cannot be decoded, that candidate does not match. Do not decode params a second time in application code.
## Keep route modules functional and explicit
The default export renders the page. Named exports add behavior without changing the route path. [`defineRoutePage`](/docs/core/api/router#api-tavojs-core-router--defineroutepage "View defineRoutePage in the Core API reference") is optional route-aware typing; it does not register the route or override its filename. Its path literal is checked with [`RouteParamsFromPath`](/docs/core/api/router#api-tavojs-core-router--routeparamsfrompath "View RouteParamsFromPath in the Core API reference"), while the filesystem remains authoritative.
**Reference src/pages/projects/[id].tsx**
```tsx
import type {
PageLoadContext,
PageProps
} from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Project = { id: string; name: string };
type ProjectParams = { id: string };
export async function load({
params,
signal,
url
}: PageLoadContext): Promise {
const response = await fetch(
new URL(`/api/projects/${params.id}`, url),
{ signal }
);
if (!response.ok) {
throw new Error("Could not load project");
}
return response.json() as Promise;
}
export function head({ data }: { data: Project | null }) {
return {data ? data.name : "Project"};
}
export default function ProjectPage({
data
}: PageProps) {
return (
{data?.name}
);
}
```
- Page modules may export load, action, middleware, head, pending, error, render, prerender, revalidate, vary, `cacheTags`, and `generateStaticParams`.
- Layout modules use the same data, middleware, head, and rendering exports and receive children from the route beneath them.
- Use [`defineRoutePage`](/docs/core/api/router#api-tavojs-core-router--defineroutepage "View defineRoutePage in the Core API reference") from @tavojs/core/router later when path-derived params and one object are clearer for a complex route.
- Keep the helper path literal aligned with the file path and inspect the generated manifest; the filesystem remains authoritative.
## Predict deterministic route precedence
Matching is independent of filesystem discovery order. Tavo.js compares each segment from left to right and tries the more specific pattern first.
**Reference Matching precedence — highest to lowest**
```text
static
→ required dynamic [id]
→ optional dynamic [[id]]
→ required catch-all [...path]
→ optional catch-all [[...path]]
/projects/new wins over /projects/[id]
/docs/[version] wins over /docs/[...path]
/files/[...path] wins over /files/[[...path]]
```
When two compiled patterns have identical specificity, Tavo.js uses a lexical path tie-break. Treat equivalent patterns as a collision to fix, not as a way to choose behavior by declaration order.
**Inspect collisions before deployment**
Run tavo routes and tavo check after adding or renaming route files. The generated route order is the behavior that development, build, and production share.
## Place 404 and error UI at the correct boundary
- src/pages/404.tsx renders when no route matches and when a loader or middleware calls [`notFound()`](/docs/core/api/router#api-tavojs-core-router--notfound "View notFound in the Core API reference"). The response status is 404.
- A page-local error export handles that page loader's failure and implements the [`PageErrorProps`](/docs/core/api/router#api-tavojs-core-router--pageerrorprops "View PageErrorProps in the Core API reference") contract: pathname, params, route layers, page data, and the error.
- `src/pages/_error.tsx` is the fallback when a page loader fails and the page has no local error export.
- A layout receives its own loader error through its error prop. Descendant loaders still run unless the failure is `notFound`(), and page pending UI is skipped while a layout error exists.
- Files whose stem begins with an underscore are not public routes. Only documented special filenames receive special behavior.
**Reference src/pages/404.tsx**
```tsx
import { Page, Text } from "@tavojs/ui";
export default function NotFoundPage({
pathname
}: {
pathname?: string;
}) {
return (
Page not found
The path {pathname ?? "you requested"} does not exist.
);
}
```
**Do not expose private failure details**
Render a stable user-facing message and report the original error through server diagnostics or instrumentation. Serialized hydration errors are redacted, but route data and custom error UI still require deliberate privacy review.
## Treat two implemented conventions as contract work in progress
Current Core source and tests recognize a top-level \_root.tsx and a page-level layout = false export. Their final public 1.0 semantics are not yet ratified.
- `_root.tsx` currently wraps every matched route before directory layouts and uses the layer ID \_root.
- layout = false currently skips directory `_layout.tsx` modules for that page while retaining `_root.tsx`.
- Do not make reusable application architecture depend on either convention until Core publishes its stable contract, inheritance rules, and migration guarantees.
- Use explicit directory layouts for production documentation and examples in the meantime.
## Verify the route graph
**Run Terminal**
```bash
npx tavo routes
npx tavo inspect route /projects/new --json
npx tavo inspect route /projects/example --json
npx tavo check
```
Verify both the static and dynamic examples so precedence is observable. Also request an unknown path and a loader path that calls `notFound`() to confirm the same 404 module and status are used.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Routing and navigation
> Navigate between typed file routes while preserving browser history, focus, scroll, and pending state.
Canonical page: https://tavojs.dev/docs/core/routing
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Navigate between typed file routes while preserving browser history, focus, scroll, and pending state.
## Use Auto Pages for application routes
Auto Pages discovers application routes from src/pages and owns matching, layouts, loaders, route pending and error views, metadata, focus restoration, accessibility announcements, and scroll restoration. Route folders, dynamic segments, groups, 404 pages, and error pages all participate in the same generated route graph.
**Standalone router**
The router entry point also exposes createRouter for small client-only or embedded flows. Most file-routed applications should use Auto Pages instead of creating a second top-level router.
## Link and scroll behavior
The Core [`Link`](/docs/core/api/router#api-tavojs-core-router--link "View Link in the Core API reference") performs SPA navigation and sets `aria-current` on the active destination. New routes begin at the top, hashes scroll into view, and browser back or forward restores saved positions.
**Create src/components/AccountNavigation.tsx — create this component**
```tsx
import { Link } from "@tavojs/core/router";
import { Inline } from "@tavojs/ui";
export function AccountNavigation() {
return
Profile
Security
;
}
```
## Prefetch intentionally
Prefetch routes when intent is clear, such as pointer hover or focus on a high-probability destination. Prefetching resolves work without changing the URL or rendering the target page's pending component. Route status APIs let interface code show loading, prefetching, ready, or error states without duplicating the router state machine.
**Create src/navigation/reports.ts — create this browser-safe helper**
```ts
import { getRouteStatus, prefetchRoute } from "@tavojs/core/router";
export async function prepareReports() {
await prefetchRoute("/reports");
return getRouteStatus("/reports");
}
```
## Subscribe and inspect responsibly
Auto Pages exposes synchronous route state and disposable subscriptions from `@tavojs/core/router`. Development inspection lives under `@tavojs/core/dev`, while `tavo` routes and `tavo` inspect route show the generated graph without adding application code.
- Use `getResolvedRoute` and `getRouteStatus` for current resolution state.
- Use `subscribeAvailableRoutes` and `subscribeRouteStatus` when interface state must react to route changes.
- Run `tavo` routes, `tavo` inspect route `--json`, or import `getAutoPagesInspection` from `@tavojs/core/dev` during development.
**Reference src/navigation/route-observer.ts**
```ts
import {
getAvailableRoutes,
getCurrentPathname,
subscribePathname
} from "@tavojs/core/router";
console.log(getCurrentPathname(), getAvailableRoutes());
const unsubscribe = subscribePathname((pathname) => {
console.log("Route changed:", pathname);
});
// Call when this observer's owner is disposed.
unsubscribe();
```
## File-router navigation API
Auto Pages discovers application routes from src/pages. Import its navigation and route-state APIs from `@tavojs/core/router`. Reads are synchronous; subscribe functions return an unsubscribe callback; `prefetchRoute` resolves route middleware and loaders without changing browser history.
- Route status is idle, loading, prefetching, ready, redirecting, or error.
- A page pending export is visible only during active client-side route resolution; `prefetchRoute` never renders it.
- Without an active resolver, `prefetchRoute` leaves the route idle rather than throwing.
- Pass an `AbortSignal` when hover, focus, or another owner should be able to cancel a prefetch.
- Call every returned unsubscribe function when its component, controller, or external owner is disposed.
- Import `getAutoPagesInspection` from `@tavojs/core/dev` for development inspection, or use `tavo` routes and `tavo` inspect route `--json`.
**Reference Reference snippet**
```ts
navigate(to: string, options?: { replace?: boolean; scroll?: boolean }): void
prefetchRoute(pathname: string, options?: { signal?: AbortSignal }): Promise
getCurrentPathname(): string
subscribePathname(listener: (pathname: string) => void): () => void
getAvailableRoutes(): PageRouteDefinition[]
subscribeAvailableRoutes(listener: (routes: PageRouteDefinition[]) => void): () => void
getResolvedRoute(pathname?: string)
getRouteStatus(pathname?: string): RouteStatus
subscribeRouteStatus(listener: (status: RouteStatus, all: RouteStatusState) => void, pathname?: string): () => void
```
## Link behavior and accessibility
- [`Link`](/docs/core/api/router#api-tavojs-core-router--link "View Link in the Core API reference") renders a real anchor and adds `aria-current`\="page" when its resolved route is active.
- Only an unmodified primary click to the same origin is intercepted. External URLs, downloads, modified clicks, new-window targets, and same-page hashes keep browser behavior.
- replace defaults to false. Navigation scrolls to the top or hash by default; scroll: false preserves the current position. Back and forward restore saved positions.
- When an i18n service is registered, internal destinations are localized and active matching ignores the locale prefix.
**Reference Reference snippet**
```ts
type LinkProps = {
to: string;
replace?: boolean;
scroll?: boolean;
className?: string;
children?: Child;
};
```
## Standalone router reference
The standalone router is intended for embedded or client-only route areas. Do not create a second top-level router inside a file-routed application.
- `RouterProvider` can render explicit children or the matched route component.
- After navigation it announces status, focuses `data-tavo-route-focus`, main, h1, or role=main, and restores scroll.
- [`navigate`](/docs/core/api/router#api-tavojs-core-router--navigate "View navigate in the Core API reference") is a no-op during server rendering. [`Router`](/docs/core/api/router#api-tavojs-core-router--router "View Router in the Core API reference") params are decoded strings.
**Reference Reference snippet**
```tsx
const router = createRouter([
{ path: "/", component: Home },
{ path: "/teams/:id", component: Team }
]);
router.navigate("/teams/core", { replace: false, scroll: true });
router.getPathname();
router.match("/teams/core"); // { route, params: { id: "core" } }
}
busy={false}
contentId="route-content"
/>
```
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Navigation and route state
> Navigate, prefetch, inspect route resolution, and own route subscriptions without leaking browser listeners.
Canonical page: https://tavojs.dev/docs/core/navigation-and-route-state
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: browser
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A file-routed Tavo.js application.
- Client-side code that needs navigation or route progress.
## Outcomes
- Choose Link or navigate without breaking browser behavior.
- Interpret every route status and prefetch outcome.
- Read initial state and clean up every route subscription.
## Use anchors first and navigate for imperative transitions
**Reference src/components/ProjectNavigation.tsx**
```tsx
import { Link, navigate } from "@tavojs/core/router";
import { Button, Stack } from "@tavojs/ui";
export function ProjectNavigation() {
function openLatestProject() {
navigate("/projects/latest");
}
return (
All projects
);
}
```
- [`Link`](/docs/core/api/router#api-tavojs-core-router--link "View Link in the Core API reference") renders a real anchor, supports keyboard and assistive technology behavior, and marks the active route with aria-current.
- Same-origin, unmodified primary clicks become client navigation. External links, downloads, modified clicks, new-window targets, and same-page hashes retain native browser behavior.
- [`navigate`](/docs/core/api/router#api-tavojs-core-router--navigate)`(``to``,` `{` `replace``,` `scroll` `}``)` is for event-driven transitions. replace defaults to false and scroll defaults to true.
- Back and forward navigation follows browser history and restores recorded scroll positions.
## Read route status as a state machine
**Reference src/navigation/progress.ts**
```ts
import {
getRouteStatus,
subscribeRouteStatus,
} from "@tavojs/core/router";
console.debug(getRouteStatus("/reports").status);
const stop = subscribeRouteStatus((status) => {
document.documentElement.dataset.routeStatus = status.status;
}, "/reports");
// Return stop from the lifecycle owner.
```
Status
Meaning
`idle`
No active or reusable resolution is recorded for the path.
`loading`
An active navigation is running middleware and loaders.
`prefetching`
Background resolution is running without changing history.
`ready`
A resolved result is available for the path.
`redirecting`
Middleware or route resolution selected another location.
`error`
Resolution failed; inspect the status error for reporting.
Each record follows [`RouteStatus`](/docs/core/api/router#api-tavojs-core-router--routestatus "View RouteStatus in the Core API reference"). Read a path with [`getRouteStatus`](/docs/core/api/router#api-tavojs-core-router--getroutestatus)`(``pathname``)` and observe future transitions with [`subscribeRouteStatus`](/docs/core/api/router#api-tavojs-core-router--subscriberoutestatus)`(``listener``,` `pathname``?``)`.
- [`getCurrentPathname`](/docs/core/api/router#api-tavojs-core-router--getcurrentpathname)`(``)` reads the current browser path.
- [`getAvailableRoutes`](/docs/core/api/router#api-tavojs-core-router--getavailableroutes)`(``)` returns discovered [`PageRouteDefinition`](/docs/core/api/router#api-tavojs-core-router--pageroutedefinition "View PageRouteDefinition in the Core API reference") records; the catalog is not an authorization boundary.
- [`getResolvedRoute`](/docs/core/api/router#api-tavojs-core-router--getresolvedroute)`(``pathname``)` returns the current resolved route snapshot when available. Pass the path explicitly when inspecting anything other than the active route. The renderer-state shape is intentionally not a separately importable route-author contract.
## Read once, subscribe, and return cleanup
[`subscribePathname`](/docs/core/api/router#api-tavojs-core-router--subscribepathname "View subscribePathname in the Core API reference"), [`subscribeAvailableRoutes`](/docs/core/api/router#api-tavojs-core-router--subscribeavailableroutes "View subscribeAvailableRoutes in the Core API reference"), and [`subscribeRouteStatus`](/docs/core/api/router#api-tavojs-core-router--subscriberoutestatus "View subscribeRouteStatus in the Core API reference") publish future changes; they do not replace the initial synchronous read. A [`createTavo`](/docs/core/api/application#api-tavojs-core--createtavo "View createTavo in the Core API reference") controller is a natural owner because onMount can return one cleanup that releases every subscription.
**Reference src/components/RouteProgress.tsx**
```tsx
import {
createTavo,
TavoController
} from "@tavojs/core";
import {
getCurrentPathname,
getRouteStatus,
subscribePathname,
subscribeRouteStatus,
type RouteStatus
} from "@tavojs/core/router";
import { Text } from "@tavojs/ui";
type RouteProgressState = {
pathname: string;
status: RouteStatus["status"];
};
class RouteProgressController extends TavoController {
sync(pathname = getCurrentPathname()) {
this.model.patch({
pathname,
status: getRouteStatus(pathname).status
});
}
onMount() {
this.sync();
const stopPathname = subscribePathname((pathname) => {
this.sync(pathname);
});
const stopStatus = subscribeRouteStatus((status) => {
this.model.patch({
pathname: status.pathname,
status: status.status
});
});
return function cleanupRouteProgress() {
stopPathname();
stopStatus();
};
}
}
export const RouteProgress = createTavo<
Record,
RouteProgressState,
RouteProgressController
>({
model: () => ({
pathname: "/",
status: "idle"
}),
controller: RouteProgressController,
view: ({ state }) => {
return (
{state.pathname}: {state.status}
);
}
});
```
**Scope status subscriptions when possible**
Pass a pathname to [`subscribeRouteStatus`](/docs/core/api/router#api-tavojs-core-router--subscriberoutestatus "View subscribeRouteStatus in the Core API reference") when one component owns one destination. An unscoped subscription receives all route transitions and should be reserved for global progress or diagnostics UI.
## Prefetch without navigating
**Reference src/components/ProjectLink.tsx**
```tsx
import { Link, prefetchRoute } from "@tavojs/core/router";
export function ProjectLink({ id }: { id: string }) {
const pathname = `/projects/${id}`;
let prefetch: AbortController | null = null;
function startPrefetch() {
prefetch?.abort();
prefetch = new AbortController();
void prefetchRoute(pathname, {
signal: prefetch.signal
});
}
function stopPrefetch() {
prefetch?.abort();
prefetch = null;
}
return (
Open project {id}
);
}
```
- `prefetchRoute` runs the target's client-eligible middleware and loaders but does not change history or render the target pending component.
- Status moves through prefetching and then ready or error. Cancellation returns the prefetch to idle.
- Pass an `AbortSignal` when hover, focus, a controller, or another owner can stop needing the result.
- Prefetch is an optimization. Navigation must still work when no prefetch ran, failed, or was evicted.
## Verify navigation and cancellation
- Replace a slow navigation with a second destination and confirm the obsolete result never becomes active.
- Abort a hover prefetch and confirm it neither changes the URL nor displays pending UI.
- Mount and unmount subscription-owning components repeatedly while checking that each cleanup runs.
- Test [`Link`](/docs/core/api/router#api-tavojs-core-router--link "View Link in the Core API reference") with keyboard activation, modifier keys, downloads, external URLs, hashes, and browser back/forward.
**Run Terminal**
```bash
npx tavo routes
npx tavo inspect route /projects/example --json
```
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Data loading and middleware
> Load route data, provide route-specific pending and error states, redirect or gate navigation, and cancel obsolete work.
Canonical page: https://tavojs.dev/docs/core/data-and-middleware
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Load route data, provide route-specific pending and error states, redirect or gate navigation, and cancel obsolete work.
## Load route-critical data
Page and layout loaders run during route resolution. Their result becomes page data and is available to route-aware controllers. A page can export pending for active browser resolution and error for its loader failure. Pass the provided `AbortSignal` to downstream work so superseded navigation cannot publish stale results.
**Replace src/pages/projects/[id].tsx — replace the route module**
```tsx
import type {
PageErrorProps,
PageLoadContext,
PagePendingProps,
PageProps
} from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Project = { name: string };
export async function load({ params, signal, url }: PageLoadContext): Promise {
const endpoint = new URL(`/api/projects/${params.id}`, url);
const response = await fetch(endpoint, { signal });
if (!response.ok) throw new Error("Project could not be loaded");
return response.json();
}
export function pending({ params }: PagePendingProps) {
return Loading project {params.id}…;
}
export function error({ pathname }: PageErrorProps) {
return Could not load {pathname}.;
}
export default function ProjectPage({ data }: PageProps) {
return {data?.name};
}
```
## Keep middleware request-scoped
Middleware runs before loaders and can continue or redirect route resolution. Return nothing to continue, or return an object with redirect and an optional status. A status without redirect does not stop the route. This example normalizes a redundant query before the loader runs. Use `defineServerMiddleware` plus a server-only helper for sessions, secrets, or other policy that must never run during browser navigation.
**Merge src/pages/projects/_layout.tsx — merge this middleware export**
```ts
import { defineServerMiddleware } from "@tavojs/core/router";
export const middleware = defineServerMiddleware(({ request }) => {
const url = new URL(request.url);
if (url.searchParams.get("view") === "all") {
return { redirect: "/projects", status: 308 };
}
});
```
## Route loader or resource?
Starting a new resource load aborts the previous one. Treat cancellation as expected control flow rather than an application error.
- Use a loader when the route cannot render meaningfully without the data.
- Use a layout loader for data shared by descendant routes.
- Use `createResource` for component-scoped async data that can load independently.
- Use lazy when the async work is loading a component implementation.
## Loader contract
- Loader runtime defaults to both. `defineServerLoader` is equivalent to a server-only loader and is skipped during browser resolution.
- request, URL, Headers, and `AbortSignal` are portable Fetch APIs. Build application-relative fetch URLs with new URL(path, url) so the same loader works in Node and the browser. Use `rawRequest` only at an adapter integration boundary.
- Pass signal to fetch and every abort-aware dependency. Superseded navigation is expected cancellation and must not publish stale data.
- Layout loaders resolve from root to leaf, followed by the page loader. Later loader contexts receive successful earlier results through the optional keyed `context.layers` record. Rendered page, pending, and error props use a different shape: ordered layers plus keyed `layerData`.
- A layout-loader failure stays on that layout layer and enters route error handling rather than rendering the page pending view with invalid layout data.
- A page-loader failure renders the target page's error export when present, then falls back to `src/pages/_error.tsx`.
- An aborted obsolete resolution returns to idle rather than rendering an error.
**Reference Reference snippet**
```ts
type PageLoadContext = {
pathname: string;
params: Record;
request: Request;
rawRequest?: unknown;
url: URL;
headers: Headers;
method: string;
signal: AbortSignal;
layers?: Record;
};
defineLoader(handler, { runtime?: "server" | "client" | "both" })
defineServerLoader(handler)
```
## Middleware contract and order
- Middleware can be declared globally, by plugins, on layouts, and on pages. Runtime-wide middleware runs first, then layout middleware from root to leaf, then page middleware.
- Return nothing to continue. Return redirect to stop normal resolution; status defaults to 302 and only has an effect when redirect is present. Returning status alone does not block a route.
- Middleware runtime defaults to both. Use `defineServerMiddleware` for `HttpOnly` cookies, secrets, databases, and server sessions.
- Redirect targets are same-origin by default. External redirects require an explicit runtime opt-in and application validation.
**Reference Reference snippet**
```ts
type PageMiddleware = ((context: {
to: string;
from?: string;
params: Record;
request: Request;
rawRequest?: unknown;
url: URL;
headers: Headers;
method: string;
signal: AbortSignal;
}) => void | { redirect?: string; status?: number } | Promise<...>) & {
__tavo_middleware_options__?: { runtime?: "server" | "client" | "both" };
};
```
## Component resource reference
- A resource starts idle with null data, error, and `updatedAt`. A new load aborts the previous load.
- preload deduplicates the current pending operation. load always starts a new one.
- abort returns to idle, clears error and `updatedAt`, and preserves the last data. reset also clears data.
- An aborted operation resolves to idle state even if the loader ignores its signal. Non-abort failures resolve to error state rather than throwing from load.
**Reference Reference snippet**
```ts
type ResourceState = {
status: "idle" | "loading" | "success" | "error";
data: T | null;
error: unknown;
updatedAt: number | null;
};
type Resource = {
store: Store>;
read(): ResourceState;
load(options?: { signal?: AbortSignal }): Promise>;
preload(options?: { signal?: AbortSignal }): Promise>;
abort(reason?: unknown): void;
reset(): void;
};
```
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Route data lifecycle
> Follow middleware, layout loaders, page loaders, pending UI, cancellation, and layer data through one route resolution.
Canonical page: https://tavojs.dev/docs/core/route-data-lifecycle
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: server, browser
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A route with a page module and, optionally, directory layouts.
- Familiarity with Fetch Request, Response, URL, Headers, and AbortSignal.
## Outcomes
- Predict middleware and loader order.
- Use the exact layer names available to loaders and components.
- Handle cancellation and each loader failure at the correct boundary.
## Follow the route resolution order
**Reference Reference snippet**
```text
matched route
→ runtime-wide middleware
→ plugin middleware
→ root and layout middleware, outer to inner
→ page middleware
→ root and layout loaders, outer to inner
→ page pending UI, only for eligible client navigation
→ page loader
→ layout and page head
→ page or page error, wrapped by resolved layouts
```
- Middleware may continue or redirect before data work starts. A redirect stops normal route resolution.
- Layout loaders run sequentially from outermost to innermost. The page loader runs last.
- Pending renders only during an active client navigation with a client-eligible page loader and no layout-loader error.
- SSR, static generation, prefetching, fresh resolved-cache hits, and routes without pending do not render page pending UI.
## Use the portable request context
Loaders implement [`PageLoader`](/docs/core/api/router#api-tavojs-core-router--pageloader "View PageLoader in the Core API reference") and receive one [`PageLoadContext`](/docs/core/api/router#api-tavojs-core-router--pageloadcontext "View PageLoadContext in the Core API reference"). The same portable request fields are inherited by [`PageActionContext`](/docs/core/api/router#api-tavojs-core-router--pageactioncontext "View PageActionContext in the Core API reference").
**Reference PageLoadContext**
```ts
type PageLoadContext = {
pathname: string;
params: Record;
request: Request;
rawRequest?: unknown;
url: URL;
headers: Headers;
method: string;
signal: AbortSignal;
layers?: Record;
};
```
- request, url, headers, and signal are portable Fetch APIs and work across supported server and browser execution.
- Build application-relative URLs with new URL(path, url), then pass signal to fetch and every abort-aware dependency.
- Use `rawRequest` only at an adapter integration boundary. It is not portable application state.
- A loader runs in both environments unless `defineLoader` selects another runtime. Use `defineServerLoader` for secrets, databases, server sessions, and `HttpOnly` cookies.
- Treat params and request data as untrusted input even when the route pattern constrained their shape.
## Distinguish loader layers from component layers
The same resolved data is exposed in two shapes for different jobs. Loader context uses the property name layers for a keyed record of successful earlier layout results. Component props use layers for the ordered diagnostic list and layerData for the keyed record.
Public contract
Property
Shape
Contents
[`PageLoadContext`](/docs/core/api/router#api-tavojs-core-router--pageloadcontext)
`layers`
`Record`
Successful earlier layout results only.
[`PageProps`](/docs/core/api/router#api-tavojs-core-router--pageprops) `/` [`PagePendingProps`](/docs/core/api/router#api-tavojs-core-router--pagependingprops) `/` [`PageErrorProps`](/docs/core/api/router#api-tavojs-core-router--pageerrorprops)
`layers`
`RouteDataLayer[]`
Ordered layout layers and, after completion, the page layer; each has id, kind, data, and error.
[`PageProps`](/docs/core/api/router#api-tavojs-core-router--pageprops) `/` [`PagePendingProps`](/docs/core/api/router#api-tavojs-core-router--pagependingprops) `/` [`PageErrorProps`](/docs/core/api/router#api-tavojs-core-router--pageerrorprops)
`layerData`
`Record`
Successful results keyed by layer ID.
- The current root layer ID is \_root.
- A directory layout ID is its src/pages-relative directory key. The root directory layout is /; `src/pages/projects/_layout.tsx` is projects; route-group names remain in the key.
- The page layer ID is the compiled route path, such as /projects/:id.
- A failed loader remains in the ordered layers array but is omitted from the successful `layerData` record.
**Reference src/pages/projects/[id].tsx**
```tsx
import type {
PageLoadContext,
PageProps
} from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Workspace = { id: string };
type Project = { id: string; name: string };
export async function load({
params,
layers,
signal,
url
}: PageLoadContext): Promise {
const workspace = layers?.projects as Workspace | undefined;
const response = await fetch(
new URL(
`/api/workspaces/${workspace?.id}/projects/${params.id}`,
url
),
{ signal }
);
if (!response.ok) {
throw new Error("Could not load project");
}
return response.json() as Promise;
}
export default function ProjectPage({
data,
layerData
}: PageProps) {
const workspace = layerData?.projects as Workspace | undefined;
return (
{workspace?.id}: {data?.name}
);
}
```
**Layer IDs are framework identities**
Directory moves and route-group renames can change layout IDs. Keep cross-layout reads local and deliberate, and cover each expected key with route inspection and integration tests.
## Handle failure and cancellation separately
- A layout-loader failure is stored on that layout layer. The failed value is not added to downstream `context.layers`, descendants continue, and the layout receives its own data and error props.
- A page-loader failure renders the page's error export when present, then `src/pages/_error.tsx`. The response defaults to status 500 unless head selects another status.
- `notFound`() from middleware or any loader stops normal output, renders `404.tsx`, and returns status 404.
- A superseding navigation aborts obsolete work. Pass signal onward and do not translate `AbortError` into user-facing route failure UI.
- A server-only loader is skipped during browser resolution; design the browser path so it already has the required hydrated or independently fetched data.
**Reference src/pages/projects/[id].tsx — failure exports**
```tsx
import {
notFound,
type PageErrorProps,
type PagePendingProps
} from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type ProjectParams = { id: string };
export async function load({ params, signal, url }) {
const response = await fetch(
new URL(`/api/projects/${params.id}`, url),
{ signal }
);
if (response.status === 404) {
notFound();
}
if (!response.ok) {
throw new Error("Could not load project");
}
return response.json();
}
export function pending({
params
}: PagePendingProps) {
return (
Loading project {params.id}…
);
}
export function error({
pathname
}: PageErrorProps) {
return (
Could not load {pathname}.
);
}
```
## Keep middleware decisions small and auditable
A route middleware export implements [`PageMiddleware`](/docs/core/api/router#api-tavojs-core-router--pagemiddleware "View PageMiddleware in the Core API reference"). Use [`defineMiddleware`](/docs/core/api/router#api-tavojs-core-router--definemiddleware "View defineMiddleware in the Core API reference") for portable work and [`defineServerMiddleware`](/docs/core/api/router#api-tavojs-core-router--defineservermiddleware "View defineServerMiddleware in the Core API reference") when the decision requires server-only state.
**Reference src/pages/account/_layout.tsx — named export**
```ts
import {
defineServerMiddleware
} from "@tavojs/core/router";
export const middleware = defineServerMiddleware(
async ({ request, signal }) => {
signal.throwIfAborted();
const { readAuthenticatedUser } = await import("src/server/auth");
const user = await readAuthenticatedUser(request, { signal });
if (!user) {
return {
redirect: "/sign-in",
status: 302
};
}
}
);
```
- Middleware runtime defaults to both. Use `defineServerMiddleware` whenever the decision reads secrets or server-only credentials.
- Return nothing to continue. Return redirect to stop resolution; its status defaults to 302.
- A status without redirect does not block or replace the route.
- External redirects are disabled by default. Keep them disabled for targets derived from request data.
## Verify order, layers, and cancellation
**Run Terminal**
```bash
npx tavo inspect route /projects/example --json
npx tavo check
npx tavo build
```
Test successful layout and page loads, each loader failing independently, notFound(), a redirect, and an aborted slow navigation. Assert both response status and the exact visible boundary rather than checking only rendered text.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Actions, forms, and validation
> Handle mutations with route actions, schema validation, explicit response shapes, and server-owned sessions.
Canonical page: https://tavojs.dev/docs/core/actions-and-forms
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Handle mutations with route actions, schema validation, explicit response shapes, and server-owned sessions.
## Put server mutations in actions
A route action handles non-GET requests in SSR mode. Parse the normalized Fetch Request, authorize the caller, perform the mutation, and return JSON, status, headers, or a redirect.
**Merge src/pages/projects/index.tsx — merge this action export**
```ts
import { defineAction } from "@tavojs/core/router";
export const action = defineAction(async ({ request }) => {
const form = await request.formData();
const name = String(form.get("name") ?? "").trim();
if (!name) {
return { status: 400, json: { error: "Project name is required" } };
}
return {
status: 201,
json: { id: crypto.randomUUID(), name }
};
});
```
## Validate input before business logic
`Tavo.js` accepts Standard Schema validators and common parse contracts. `defineValidatedAction` parses JSON or form input, returns HTTP 400 for invalid input, and passes typed input to the handler.
**Merge src/pages/projects/new.tsx — merge this action export**
```ts
import { defineValidatedAction } from "@tavojs/core/dev";
type ProjectInput = { name: string };
type ProjectParseResult =
| { success: true; data: ProjectInput }
| { success: false; error: { issues: Array<{ message: string; path: string[] }> } };
const projectSchema = {
safeParse(value: unknown): ProjectParseResult {
const name =
value && typeof value === "object"
? (value as Record).name
: undefined;
if (typeof name !== "string" || name.trim().length < 2) {
return {
success: false,
error: {
issues: [{
message: "Project name must contain at least two characters",
path: ["name"]
}]
}
};
}
return { success: true, data: { name: name.trim() } };
}
};
export const action = defineValidatedAction(projectSchema, async ({ input }) => {
return {
status: 201,
json: { id: crypto.randomUUID(), name: input.name }
};
});
```
## Use a safe mutation order
- Validate the input shape.
- Authenticate and authorize the request.
- Apply origin, CSRF, and idempotency rules appropriate to the endpoint.
- Commit the database or external side effect.
- Return only safe response data.
**Validation is not authorization**
A valid payload can still come from the wrong user. Keep permissions and session checks inside the server action before state changes.
## Route action contract
- Route actions handle non-GET requests during SSR route handling. The context is the same portable request context used by loaders.
- Return a Response directly or an `ActionResult`. redirect creates a Location response; json serializes a JSON body; status and headers customize the response.
- Unsafe methods validate browser Origin by default. Set `validateOrigin`: false only for endpoints with independent authentication such as signed webhooks.
- When `contentType` is declared, a mismatched request receives 415 Unsupported Media Type before the handler runs.
**Reference Reference snippet**
```ts
type ActionResult = {
body?: BodyInit | null;
headers?: HeadersInit | Record;
json?: unknown;
redirect?: string;
status?: number;
};
defineAction(handler, {
contentType?: "form-data" | "json";
validateOrigin?: boolean;
});
```
## Client action state
- run aborts an older run and resolves to the final `ActionState`. Handler failures become error state; run does not rethrow them.
- abort returns to idle, clears error and `completedAt`, and preserves existing data. reset aborts and clears the complete state.
- Subscribe through `action.store` or a controller's listen/select helpers.
**Reference Reference snippet**
```ts
type ActionState = {
status: "idle" | "running" | "success" | "error";
data: TResult | null;
error: unknown;
submittedAt: number | null;
completedAt: number | null;
};
const save = createAction(async ({ input, signal }) => saveProject(input, { signal }));
save.store; // observable Store>
save.getState();
await save.run(input);
save.abort();
save.reset();
```
## Form helpers and transport defaults
- Repeated `FormData` field names become arrays; single fields remain one `FormDataEntryValue`.
- Server forms default to POST, multipart `FormData`, and same-origin credentials. JSON mode adds application/json unless a content type already exists.
- The default parser throws for a non-ok response, otherwise returns JSON when declared by the response and text for other content types.
- body and `contentType` may be selected by boot mode. A redirected browser response is followed with `window.location.assign`.
**Reference Reference snippet**
```ts
formDataToObject(data: FormData): FormValues
createFormAction(handler): FormAction
createServerFormAction(url, {
body?: "form-data" | "json" | ((values, context) => BodyInit);
contentType?: "form-data" | "json";
credentials?: RequestCredentials;
fetch?: typeof fetch;
headers?: HeadersInit;
method?: string;
parseResponse?: (response: Response) => MaybePromise;
}): FormAction
type FormAction = {
action: Action;
store: Store>;
submit(form: HTMLFormElement | FormData | FormValues): Promise>;
reset(): void;
};
```
## Validation schemas and failures
`validateInput` accepts Standard Schema, `safeParse` or `safeParseAsync`, and parse or `parseAsync` contracts. `defineValidatedAction` reads JSON when Content-Type includes application/json and otherwise converts `FormData` while preserving repeated fields.
**Reference Reference snippet**
```ts
const result = await validateInput(schema, unknownInput);
// { ok: true, value } | { ok: false, issues: [{ message, path? }] }
export const action = defineValidatedAction(schema, async ({ input, request }) => {
await authorize(request, input);
return { status: 201, json: await createRecord(input) };
});
// Invalid input response, status 400:
// { error: "validation_failed", issues: [{ message, path? }] }
```
**Validation is not authorization**
Schema validation proves the payload shape only. Authenticate, authorize, apply CSRF or idempotency rules, and then commit the mutation inside the server action.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Route actions and CSR forms
> Handle mutations with route actions, predictable response defaults, origin checks, and explicit pure-CSR form transport.
Canonical page: https://tavojs.dev/docs/core/route-actions-and-csr-forms
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: server, browser
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A route that accepts a non-GET request.
- Understanding of HTTP methods, status codes, origins, and form encoding.
## Outcomes
- Return the intended HTTP response from every action result form.
- Preserve origin, content-type, authentication, and authorization checks.
- Configure form transport for a pure CSR deployment without double handling.
## Define the mutation beside its route
A route action implements [`PageAction`](/docs/core/api/router#api-tavojs-core-router--pageaction "View PageAction in the Core API reference"), receives [`PageActionContext`](/docs/core/api/router#api-tavojs-core-router--pageactioncontext "View PageActionContext in the Core API reference"), and may return a native Response or the documented data, redirect, or error object forms. [`defineAction`](/docs/core/api/router#api-tavojs-core-router--defineaction "View defineAction in the Core API reference") adds origin and content-type policy to that handler.
**Reference src/pages/projects/new.tsx**
```tsx
import {
defineAction,
type PageActionContext
} from "@tavojs/core/router";
import { Button, Page, Stack, TextField } from "@tavojs/ui";
type NewProjectInput = { name: string };
export const action = defineAction(
async ({ request }: PageActionContext) => {
const {
authorizeProjectCreation,
createProject,
requireUser
} = await import("src/server/projects");
const form = await request.formData();
const input: NewProjectInput = {
name: String(form.get("name") ?? "").trim()
};
const user = await requireUser(request);
authorizeProjectCreation(user);
const project = await createProject(input);
return {
redirect: `/projects/${project.id}`
};
},
{
contentType: "form-data"
}
);
export default function NewProjectPage() {
return (
);
}
```
- The Node route handler sends non-GET and non-HEAD requests to the matched page action before normal page rendering.
- If the route has no action, the handler returns 405 Method Not Allowed with Allow: GET, HEAD.
- A body above the configured request limit returns 413. An unhandled action failure becomes a generic 500 response.
- Return a Response to control the full response, or return an `ActionResult` for `Tavo.js` to normalize and harden.
## Know the action response defaults
Handler result
Default response
`undefined`
204 with an empty body
`{ redirect }`
303 with Location
`{ json }`
200 with application/json; charset=utf-8 and a serialized body
`{ body }`
200 when body is present; otherwise 204
`Response`
Its status, body, and headers are preserved; default security headers are added when absent
An explicit status or headers value in these object forms overrides the corresponding default. External redirect targets are rejected unless the runtime explicitly enables them; validate any user-derived redirect again at the application boundary.
## Apply transport checks before business logic
- Unsafe action methods validate Origin against the request origin by default. Node-like requests also require a local or configured trusted host.
- Do not set `validateOrigin`: false for a browser form. Reserve it for endpoints with an independent authenticity mechanism, such as a verified webhook signature.
- `contentType`: "json" accepts application/json and +json media types. `contentType`: "form-data" accepts multipart/form-data and application/x-www-form-urlencoded.
- A declared content-type mismatch returns 415 before the handler runs.
- Parsing or schema validation proves shape, not identity or permission. Authenticate, authorize, enforce CSRF or idempotency policy, and then commit the mutation.
- Do not return secrets, private exception messages, or raw database failures in action bodies.
**Trusted hosts are not user authorization**
trustedHosts establishes which request host is acceptable for origin comparison. It does not authenticate the caller or grant permission to mutate a record.
## Configure delegated forms only for pure CSR boot
In a server-rendered document, a normal form posts to its matched route action. A pure CSR document can opt into delegated action transport through [`bootTavo`](/docs/core/api/application#api-tavojs-core--boottavo "View bootTavo in the Core API reference") and its `csrActions` option so the browser submits to an available backend endpoint.
**Reference src/main.tsx — pure CSR entry**
```tsx
import { bootTavo } from "@tavojs/core";
void bootTavo({
csrActions: {
enabled: true,
baseUrl: "https://api.example.com",
credentials: "include",
headers: {
"X-Requested-With": "Tavo.js"
}
}
});
```
**Reference CsrActionsOptions — developer-supplied callbacks**
```ts
type CsrActionContext = {
pathname: string;
search: string;
form?: HTMLFormElement;
};
type CsrActionsOptions = {
enabled?: boolean;
baseUrl?: string;
resolveUrl?: (context: CsrActionContext) => string;
credentials?: RequestCredentials;
headers?:
| HeadersInit
| ((context: {
pathname: string;
form: HTMLFormElement;
}) => HeadersInit);
};
```
- Delegation applies to non-GET, same-window forms that are not explicitly external.
- The browser sends `FormData` with the form method. credentials defaults to include when not configured.
- `data-tavo-native` opts one form out so the browser performs its native submission.
- A same-origin redirect becomes replace navigation. Other redirect destinations use a full browser navigation.
- CSR action delegation is not enabled during SSR hydration. This prevents the hydrated application from installing a competing form transport.
- A pure CSR form without `csrActions` emits a development warning unless it is marked `data-tavo-native`.
**CORS does not replace action security**
A cross-origin CSR backend must deliberately allow the app origin and credentials. The action still needs origin or signature validation, authentication, authorization, and safe cookie settings.
## Verify the complete mutation path
- Submit valid multipart and URL-encoded forms, then test the wrong media type and expect 415.
- Send a missing or cross-origin Origin header according to the deployment contract and verify 403 where required.
- Verify no action returns 405, an oversized body returns 413, and internal failures return a generic 500.
- Test redirect, JSON, body, empty, and direct Response results with their exact status and headers.
- For pure CSR, verify endpoint mapping, credentials, native opt-out, same-origin replace navigation, and full external navigation.
**Run Terminal**
```bash
npx tavo inspect route /projects/new --json
npx tavo build
```
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# MVC components
> Separate local reactive state, business behavior, and TSX rendering with createTavo.
Canonical page: https://tavojs.dev/docs/core/mvc
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Separate local reactive state, business behavior, and TSX rendering with createTavo.
## Give each concern one home
A `Tavo.js` component has an optional model, an optional controller, and a required view. The model stores renderable local state, the controller coordinates behavior and side effects, and the view describes output from props and state.
- Render-only components can provide only a view.
- Small interactions can patch the model directly.
- Use a controller when behavior needs lifecycle, services, stores, routing, refs, or managed cleanup.
## Build a controller-backed component
Controllers receive the component model and current props automatically. They also have access to page, router, stores, and services supplied by the runtime.
**Create src/components/Counter.tsx — create this component**
```tsx
import { createTavo, TavoController } from "@tavojs/core";
import { Button } from "@tavojs/ui";
class CounterController extends TavoController {
increment() {
this.model.patch((state) => ({ count: state.count + 1 }));
}
}
export const Counter = createTavo({
model: () => ({ count: 0 }),
controller: CounterController,
view: ({ state, controller }) => (
)
});
```
## Own lifecycle work
Use `onInit` for subscriptions and initial model work, `onMount` or `onLayout` for DOM-dependent behavior, `afterRender` for post-commit work, and `onPropsChange` for explicit prop reactions.
**Reference src/components/Clock/controller.ts**
```ts
import { TavoController } from "@tavojs/core";
export class ClockController extends TavoController {
onMount() {
const timer = window.setInterval(() => {
this.model.patch({ now: new Date() });
}, 1000);
return () => window.clearInterval(timer);
}
}
```
**Cleanup is part of the feature**
Return cleanup from mount methods or register it with this.cleanup(). Prefer controller helpers such as listen, select, observeResize, and observeIntersection so teardown follows the component lifecycle.
## Understand the complete createTavo contract
[`createTavo`](/docs/core/api/application#api-tavojs-core--createtavo "View createTavo in the Core API reference") returns a typed component. Only view is required. A model owns reactive state, while a controller owns behavior and managed side effects. Each mounted component keeps one model and one controller instance across rerenders.
**Reference Reference snippet**
```ts
createTavo({
model?: (props: Props) => State | Store,
controller?: new (ctx: MvcControllerContext) => Controller,
createController?: (ctx: MvcControllerContext) => Controller,
view: (ctx: {
props: Props;
state: State;
model: Store;
controller: Controller | null;
}) => Child
}): Component
```
## model
Runs once for a mounted component, using its initial props. Return a plain object for Tavo.js to wrap with createStore, or return an existing Store. When omitted, Tavo.js creates an empty Store.
## controller
A class constructor that receives the complete controller context. Use it for reusable behavior, lifecycle methods, and managed work.
## createController
A factory alternative for functional or object-based controllers. If both controller fields are supplied, the class controller is used.
## view
Receives current props, the state snapshot, the model Store, and the controller instance. Without a controller, controller is null.
**Reference Reference snippet**
```tsx
import { createTavo } from "@tavojs/core";
import { Button } from "@tavojs/ui";
export const Counter = createTavo({
model: ({ initial = 0 }: { initial?: number }) => ({ count: initial }),
view: ({ state, model }) => (
)
});
```
**Initial props and current props are different concerns**
model(props) runs only when the component instance is created. Use controller.props or onPropsChange when behavior must follow later prop updates.
## Use the context injected into every controller
[`TavoController`](/docs/core/api/application#api-tavojs-core--tavocontroller "View TavoController in the Core API reference") receives six developer-facing context groups. They are also available on the ctx constructor argument and on controllers returned by createController.
Property
Value
Use
`model`
[`Store`](/docs/core/api/application#api-tavojs-core--store)`<``State``>`
Read and update the component-local reactive model.
`props`
`Props`
Read the latest props; Tavo.js refreshes them before each view call.
`router`
[`Router`](/docs/core/api/router#api-tavojs-core-router--router) `helpers`
Navigate, update the URL, prefetch, and inspect available routes.
`stores`
`Global store registry`
Get, test, and list stores previously created with defineGlobalStore.
`services`
`Service registry`
Get, optionally resolve, test, and list named or typed services.
`page`
`Current route state`
Read pathname, status, params, data, errors, and layout-layer data.
**Reference Reference snippet**
```ts
class ProjectController extends TavoController {
openSettings() {
this.router.navigate(`/projects/${this.page.params.id}/settings`);
}
rememberTab(tab: string) {
// Changes browser history without running route navigation or remounting.
this.router.pushUrl(`?tab=${encodeURIComponent(tab)}`);
}
prefetchReports(signal?: AbortSignal) {
return this.router.prefetch("/reports", { signal });
}
}
```
Context
Available members
`router`
[`navigate`](/docs/core/api/router#api-tavojs-core-router--navigate)`(``to``,` `options``?``)``,` `pushUrl``(``to``)``,` `replaceUrl``(``to``)``,` `prefetch``(``pathname``,` `{` `signal``?` `}``?``)``,` `routes`
`stores`
`get(name), has(name), list()`
`services`
`get(identifier), tryGet(identifier), has(identifier), list()`
`page`
`pathname, route, status, data, params, error, layers, layerData`
**Lookup does not register**
Controller stores and services expose lookup APIs only. Define a global store with defineGlobalStore and register a service before a controller resolves it. stores.get and services.get throw when a name is missing; services.tryGet returns undefined.
## Let the controller manage side effects
Controller helpers return unsubscribe functions. You can call one early to cancel the work; otherwise Tavo.js disposes it when the component is destroyed.
Method
Behavior
`cleanup(fn)`
Register any cleanup and receive an idempotent wrapped unsubscribe.
`createId(prefix?)`
Create an instance-scoped sequential ID. The default prefix is id.
`setTimeout(fn, delay?)`
Schedule a timeout that is removed after firing and cancelled on destroy.
`setInterval(fn, delay?)`
Schedule an interval that is cancelled on destroy.
`scheduleLayoutEffect(fn)`
Queue managed microtask work that may return cleanup.
`scheduleAfterRender(fn)`
Queue one-shot microtask work and unregister after it runs.
`scheduleOnMount(fn)`
Queue managed mount work that may return cleanup.
`listen(store, listener, options?)`
Subscribe to a complete Tavo.js Store snapshot.
`select(store, selector, listener, options?)`
Subscribe to a selected value with optional equality.
`watch(store, target, listener, options?)`
Watch a key, nested path, or selector.
`listenExternal(store, listener, options?)`
Subscribe to an ExternalStore snapshot with optional equality.
[`observeResize`](/docs/core/api/components-and-dom#api-tavojs-core--observeresize)`(``target``,` `listener``,` `options``?``)`
Create a managed ResizeObserver.
[`observeIntersection`](/docs/core/api/components-and-dom#api-tavojs-core--observeintersection)`(``target``,` `listener``,` `options``?``)`
Create a managed IntersectionObserver.
[`observeMutation`](/docs/core/api/components-and-dom#api-tavojs-core--observemutation)`(``target``,` `listener``,` `options``?``)`
Create a managed MutationObserver for a Node or ref object.
`action(fn)`
Wrap sync or async work with reactive pending, result, and error state.
**Reference Reference snippet**
```tsx
class PanelController extends TavoController {
declare panel: { current: HTMLElement | null };
onMount() {
this.setInterval(() => this.refresh(), 30_000);
this.observeResize(this.panel, () => this.measure());
return this.listen(filters, (state) => this.applyFilters(state));
}
refresh() {}
measure() {}
applyFilters(_state: FilterState) {}
}
```
## Choose the lifecycle hook by timing
Hook
When it runs
Return
SSR
`onInit()`
Once in the first passive mount task, immediately before onMount.
No
No
`onMount()`
Once in the first passive mount task.
May return cleanup
No
`onLayout()`
After every client commit, before passive hooks.
May return cleanup
No
`afterRender()`
As a passive task after every client commit.
No
No
`onPropsChange(props)`
Initial client render and later shallowly changed props.
No
No
`onDestroy()`
Once during teardown, before managed controller cleanups flush.
No
No
onLayout cleanup follows layout rerenders and unmount. An onMount cleanup is registered with the controller automatically. Controller props are updated synchronously before view renders; onPropsChange is the later passive notification and uses top-level shallow equality.
**Reference Reference snippet**
```ts
class DialogController extends TavoController {
onInit() {
this.model.patch({ phase: "ready" });
}
onLayout() {
const restore = captureFocusRestore();
return () => restore();
}
onMount() {
return this.listen(preferences, ({ reducedMotion }) => {
this.model.patch({ reducedMotion });
}, { immediate: true });
}
afterRender() {
// Observe the committed client view.
}
onPropsChange(props: DialogProps) {
if (!props.open) this.model.patch({ phase: "closed" });
}
onDestroy() {
// Final controller-owned work. Managed helpers are cleaned next.
}
}
```
**What participates in server rendering**
Model creation, controller construction, view rendering, and IDs created from the constructor context participate in SSR. Client lifecycle hooks, timers, observers, and DOM-dependent work do not.
## Represent async work with controller actions
**Reference Reference snippet**
```tsx
class ProfileController extends TavoController {
save = this.action(async (name: string) => {
const response = await fetch("/api/profile", {
method: "POST",
body: JSON.stringify({ name })
});
if (!response.ok) throw new Error("Could not save profile");
return response.json() as Promise<{ name: string }>;
});
}
export const Profile = createTavo({
controller: ProfileController,
view: ({ controller }) =>
{controller?.save.error ? Save failed : null}
{controller?.save.result ? Saved {controller.save.result.name} : null}
});
```
- An action starts with pending false, error null, and result null.
- run sets pending true and clears error. It resolves with the function result or rethrows the caught error.
- Only the most recently started run may update action state, so an older response cannot overwrite a newer one.
- Starting another run keeps the previous result visible while pending.
- reset clears pending, error, and result and prevents in-flight completions from changing action state.
- Every action-state transition rerenders the owning MVC component.
**Actions are safe as class fields**
action is the one TavoController helper designed to run from a class-field initializer before normal controller attachment. Await or catch run because rejected work is rethrown to its caller.
## Use constructor context before attachment
Tavo.js constructs a class with the complete ctx argument, then attaches model, props, framework context, and managed methods to the instance. Use ctx for constructor-time IDs or reads. Use this.model, this.props, and the other instance helpers from lifecycle hooks and normal methods.
**Reference Reference snippet**
```tsx
import { Box, TextInput } from "@tavojs/ui";
class FieldController extends TavoController {
id: string;
constructor(ctx: { createId(prefix?: string): string }) {
super();
this.id = ctx.createId("field");
}
}
export const Field = createTavo, Record, FieldController>({
controller: FieldController,
view: ({ controller }) => (
Email
)
});
```
**Do not call most this helpers in the constructor**
this.createId, cleanup, timers, schedulers, listeners, and observers are unavailable until createTavo finishes attachment and will throw. Use ctx in the constructor, or move the work to a lifecycle hook.
## Avoid common MVC mistakes
## Recreating state from new props
model runs once per component instance. React to later props through current controller.props or onPropsChange.
## Leaking manual subscriptions
Prefer listen, select, watch, listenExternal, and observer helpers. Otherwise pass the unsubscribe to cleanup.
## Using URL updates as navigation
pushUrl and replaceUrl only change browser history. Use navigate when the application should resolve and render another route.
## Ignoring action rejection
Action error state is reactive, but run still rejects. Await it in a try/catch or attach a catch handler from an event callback.
## Doing DOM work during SSR
Put element measurement, focus, observers, and browser timers in onLayout or onMount rather than constructors or model factories.
## Duplicating model state
Read renderable state from view.state and mutate through view.model or controller.model instead of copying snapshots into controller fields.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Stores
> Model shared state with explicit actions, precise subscriptions, derived values, and optional persistence.
Canonical page: https://tavojs.dev/docs/core/stores
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Model shared state with explicit actions, precise subscriptions, derived values, and optional persistence.
## Keep state and mutations together
`createStore` accepts an object or an initializer. Prefer an initializer when the store owns actions: callers then express intent through methods instead of coordinating partial state writes throughout the application.
**Create src/stores/filters.ts — create this store**
```ts
import { createStore } from "@tavojs/core";
export const filters = createStore((set, get) => ({
status: "all",
setStatus(status: string) { set({ status }); },
reset() { set({ status: "all" }); },
isActive() { return get().status !== "all"; }
}));
```
## Subscribe to the smallest useful value
Use patch for top-level partial updates, set for a key or nested path, and `setState` only when replacing the complete state value. `subscribeSelector` and watch prevent unrelated changes from waking application behavior.
**Reference src/stores/filters.ts — append while learning subscriptions**
```ts
const stop = filters.subscribeSelector(
(state) => state.status,
(nextStatus, previousStatus) => console.log({ nextStatus, previousStatus })
);
filters.set("status", "active");
stop();
```
## Choose the correct scope
Use a component model for state owned by one component, a global store for client state shared across routes, and a computed store for a derived value consumed by multiple subscribers.
**SSR safety**
Global stores are process-wide on the server. Never place the current request user, session, token, tenant, or permissions in them. Return safe request data from a server loader instead.
## Use the complete Store contract
**Reference Reference snippet**
```ts
type Store = {
getState(): State;
setState(next: State | ((previous: State) => State)): State;
set(path, valueOrUpdater): State;
patch(partial: Partial | ((previous: State) => Partial)): State;
subscribe(listener, options?): Unsubscribe;
subscribeSelector(selector, listener, options?): Unsubscribe;
watch(pathOrSelector, listener, options?): Unsubscribe;
};
```
Method
Use it to
`getState()`
Read the current complete snapshot.
`setState(next)`
Replace the complete state with a value or updater result.
`patch(partial)`
Shallow-merge a top-level partial value or updater result.
`set(path, value)`
Immutably update one key or nested path; the updater receives the previous value and whole state.
`subscribe(listener)`
Observe every emitted complete-state write.
`subscribeSelector(selector, listener)`
Observe a computed selection and ignore equal results.
`watch(target, listener)`
Observe a key, nested path, or selector with complete previous-state context.
**Reference Reference snippet**
```ts
const settings = createStore({
theme: "system" as "light" | "dark" | "system",
profile: { name: "Ada", notifications: true }
});
settings.set("theme", "dark");
settings.set("profile.notifications", (enabled) => !enabled);
settings.patch((state) => ({
profile: { ...state.profile, name: "Grace" }
}));
settings.setState({
theme: "system",
profile: { name: "Ada", notifications: true }
});
```
**Replacement and patching are not interchangeable**
setState replaces the entire state. patch merges only the top level. set clones the containers on one path. All update methods return the resulting snapshot.
## Keep state and actions together
**Reference Reference snippet**
```ts
import { createStore } from "@tavojs/core";
export const cart = createStore((set, get) => ({
items: [] as Array<{ id: string; quantity: number }>,
add(id: string) {
const current = get().items;
set({ items: [...current, { id, quantity: 1 }] });
},
clear() {
set({ items: [] });
}
}));
cart.getState().add("keyboard");
```
In an initializer, set is the Store's top-level patch function and get reads the complete snapshot. Capture them in action methods that run later. Action functions remain part of the Store state at runtime.
**Do not call set or get while creating initial state**
The Store is not initialized until the initializer returns. Calling set() or get() synchronously inside the initializer throws. Defining methods that call them later is the intended pattern.
## Update nested paths immutably
**Reference Reference snippet**
```ts
const board = createStore({
columns: [
{ title: "Todo", cards: [{ id: "a", done: false }] }
]
});
board.set(["columns", 0, "title"], "In progress");
board.set(["columns", 0, "cards", 0, "done"], true);
board.set("columns.0.cards.0.done", (done) => !done);
```
- set accepts a top-level key, a dot-separated path, a number, or a readonly segment array.
- The updater receives the previous selected value and the complete state.
- `Tavo.js` clones every ancestor on the path and preserves unrelated references.
- Missing containers become arrays when the next segment is numeric and objects otherwise.
- If the selected value is unchanged under `Object.is`, `Tavo.js` returns the original state and emits nothing.
- A real top-level key containing dots takes precedence over interpreting that string as a path.
**Unsafe path segments are rejected**
\_\_proto\_\_, prototype, and constructor are not allowed in Store paths. This prevents nested writes from becoming prototype pollution.
## Subscribe at the smallest useful level
API
Target
Listener
Notification
`subscribe`
Complete state
`(state, previousState)`
Every emitted write
`subscribeSelector`
Selector
`(selected, previousSelected, state)`
Object.is by default
`watch`
Key, path, or selector
`(selected, previousSelected, state, previousState)`
Object.is by default
**Reference Reference snippet**
```ts
const stopName = account.watch(
"user.profile.name",
(name, previousName, state, previousState) => {
console.log({ name, previousName, state, previousState });
},
{ immediate: true }
);
const stopSummary = account.subscribeSelector(
(state) => ({ name: state.user.profile.name, plan: state.plan }),
(summary, previousSummary) => console.log(summary, previousSummary),
{ isEqual: shallowEqual }
);
stopName();
stopSummary();
```
With immediate enabled, Tavo.js invokes the listener at subscription time. Current and previous selected values are the same current value; state and previousState are also the same current snapshot where supplied.
**Selectors that return objects need equality**
Object.is treats each newly created object as different. Supply shallowEqual or a domain-specific comparator when a selector returns an object or array and unrelated writes should be ignored.
## Derive and persist focused state
**Reference Reference snippet**
```ts
import { computedStore, persistStore } from "@tavojs/core";
const account = createStore({ first: "Ada", last: "Lovelace", token: "secret" });
export const displayName = computedStore(account, (state) => ({
value: `${state.first} ${state.last}`
}));
const stopPersistence = persistStore(account, {
key: "account-preferences",
pick: ({ first, last }) => ({ first, last })
});
// Stop writing future changes when this persistence owner is disposed.
stopPersistence();
```
## computedStore
Creates a Store from the selector's current result and updates it when the source selection changes. Pass isEqual when the selection needs custom comparison.
## persistStore
Reads an existing saved value immediately, patches it into the Store, and subscribes future writes. It defaults to localStorage and JSON.
- `persistStore` requires key and accepts storage, serialize, deserialize, and pick overrides.
- Without browser storage or a supplied storage adapter, it returns a no-op unsubscribe.
- It does not save initial state until a [`Store`](/docs/core/api/application#api-tavojs-core--store "View Store in the Core API reference") write emits.
- Storage and parse errors are not swallowed; handle them in a custom adapter when recovery is required.
- `computedStore` returns the normal [`Store`](/docs/core/api/application#api-tavojs-core--store "View Store in the Core API reference") interface. Treat it as derived output and update its source instead.
**Persist preferences, not secrets**
Browser storage is readable by client JavaScript. Use pick to exclude tokens, private server data, and fields that should expire with the session.
## Adapt state owned outside Tavo.js
**Reference Reference snippet**
```ts
import { createExternalStore } from "@tavojs/core";
export const colorScheme = createExternalStore({
getSnapshot: () => matchMedia("(prefers-color-scheme: dark)").matches,
getServerSnapshot: () => false,
subscribe(listener) {
const query = matchMedia("(prefers-color-scheme: dark)");
query.addEventListener("change", listener);
return () => query.removeEventListener("change", listener);
}
});
class ThemeController extends TavoController {
onMount() {
return this.listenExternal(
colorScheme,
(dark) => this.model.patch({ dark }),
{ immediate: true }
);
}
}
```
An ExternalStore provides getSnapshot, subscribe, and an optional getServerSnapshot. createExternalStore returns that interface unchanged. Controller listenExternal reads getSnapshot, suppresses equal values with Object.is by default, and manages unsubscription.
## Share named client state across routes
**Reference Reference snippet**
```ts
import {
defineGlobalStore,
getGlobalStore,
hasGlobalStore,
listGlobalStores
} from "@tavojs/core";
export const preferences = defineGlobalStore("preferences", (set) => ({
density: "comfortable" as "comfortable" | "compact",
setDensity(density: "comfortable" | "compact") {
set({ density });
}
}));
class ToolbarController extends TavoController {
compact() {
this.stores.get>("preferences")
.getState()
.setDensity("compact");
}
}
```
- `defineGlobalStore` creates a name once. A later definition returns the existing [`Store`](/docs/core/api/application#api-tavojs-core--store "View Store in the Core API reference") and ignores the new initial state.
- `getGlobalStore` throws for an unknown name; `hasGlobalStore` checks first and `listGlobalStores` returns registered names.
- Controller `this.stores` exposes get, has, and list but does not define stores.
- Use the `@tavojs/core` package root for the complete global-store API.
**Global means process-wide during SSR**
Never write the current user, session, token, tenant, or permissions into a global Store during server rendering. Tavo.js warns once per global Store name when setState, set, or patch writes during SSR, but the write can still leak request data. Return request-scoped values from loaders instead.
## Understand automatic Store hydration
Tavo.js serializes written Store state during SSR, embeds it in the page, and hydrates matching client Stores. Top-level function values are excluded from the serialized snapshot. This lifecycle is automatic; snapshot scopes and document-state hydration are framework internals, not application APIs.
**Keep request values request-scoped**
Use route loader data for the current user, session, token, tenant, permissions, and other request-specific values. Never write those values to a process-wide global Store during server rendering.
## Avoid common Store mistakes
## Mutating nested state
Use set with a path or replace ancestors in patch. Direct mutation bypasses immutable comparison and notifications.
## Replacing state with patch
patch preserves unspecified top-level fields. Use setState only when the complete snapshot is intentionally replaced.
## Object selectors without equality
A new object fails Object.is on every write. Pass shallowEqual or a focused comparator.
## Forgetting unsubscribe
Keep the function returned by subscriptions and persistence. Inside controllers, prefer managed listen, select, and watch helpers.
## Calling initializer helpers immediately
Define action functions that call set and get later. The Store is not readable or writable until its initializer returns.
## Putting request data in a global Store
Global Stores are process-wide on the server. Keep identity and authorization data in request-scoped loader results.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Services and dependency lifetimes
> Register typed application dependencies, resolve them through controllers, and keep process-wide services free of request identity.
Canonical page: https://tavojs.dev/docs/core/services-and-dependencies
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- An application dependency that is not component state or request data.
## Outcomes
- Register and resolve named or typed application services.
- Choose between models, Stores, services, and plugin capabilities.
- Keep server request state isolated from process-wide registries.
## Choose the dependency owner first
- Use a [`createTavo`](/docs/core/api/application#api-tavojs-core--createtavo "View createTavo in the Core API reference") model for reactive state owned by one mounted component.
- Use a global [`Store`](/docs/core/api/application#api-tavojs-core--store "View Store in the Core API reference") for observable browser state shared across multiple component or route owners.
- Use an application service for a stable dependency such as a client SDK, stateless formatter, telemetry sink, or feature client.
- Use a plugin capability for behavior owned and declared by a Plugin API v1 installation.
- Use loader, middleware, action, endpoint, or session context for the current request, user, tenant, token, or permissions.
**Reference src/services/clock.ts**
```ts
import {
createServiceKey,
registerService,
} from "@tavojs/core";
export type Clock = {
now(): Date;
};
export const clockKey = createServiceKey("app.clock");
registerService(clockKey, {
now: () => new Date(),
});
```
## Register and resolve a typed service
[`createServiceKey`](/docs/core/api/application#api-tavojs-core--createservicekey "View createServiceKey in the Core API reference") carries the service type while retaining a stable string name. Register once in the runtime that owns the dependency with [`registerService`](/docs/core/api/application#api-tavojs-core--registerservice "View registerService in the Core API reference"), then resolve by the same key.
- [`getService`](/docs/core/api/application#api-tavojs-core--getservice "View getService in the Core API reference") and controller services.get throw when the identifier is missing.
- [`tryGetService`](/docs/core/api/application#api-tavojs-core--trygetservice "View tryGetService in the Core API reference") and controller services.tryGet return undefined for an optional dependency.
- [`hasService`](/docs/core/api/application#api-tavojs-core--hasservice "View hasService in the Core API reference") checks one identifier; [`listServices`](/docs/core/api/application#api-tavojs-core--listservices "View listServices in the Core API reference") returns every registered name.
- String names are supported, but a typed key keeps registration and lookup aligned without repeated generic arguments.
**Reference src/services/metrics.ts**
```ts
import {
createServiceKey,
getService,
hasService,
registerService,
} from "@tavojs/core";
export type MetricsService = {
increment(name: string): void;
};
export const metricsKey =
createServiceKey("app:metrics");
function createMetricsService(): MetricsService {
return {
increment(name) {
navigator.sendBeacon(
"/api/metrics",
JSON.stringify({ name }),
);
},
};
}
export function installMetricsService(): MetricsService {
if (!hasService(metricsKey)) {
registerService(metricsKey, createMetricsService());
}
return getService(metricsKey);
}
```
**Reference src/main.tsx**
```tsx
import { bootTavo } from "@tavojs/core";
import { installMetricsService } from "./services/metrics";
installMetricsService();
void bootTavo().catch((error: unknown) => {
console.error("Tavo.js failed to start.", error);
});
```
**Reference src/components/TrackedButton.tsx**
```tsx
import {
createTavo,
TavoController,
} from "@tavojs/core";
import { metricsKey } from "../services/metrics";
class TrackedButtonController extends TavoController {
track() {
this.services.get(metricsKey).increment("project_created");
}
}
export const TrackedButton = createTavo({
controller: TrackedButtonController,
view({ controller }) {
return (
);
},
});
```
## Make replacement intentional
- `registerService` returns the same service instance it registers.
- A duplicate name still replaces the previous value. Core warns once unless { override: true } declares that replacement is intentional.
- The default i18n service uses the reserved `tavo`:i18n name and participates in framework discovery.
- `unregisterService` and `clearServices` are development/testing helpers from `@tavojs/core/dev`; use them to isolate tests rather than as normal application lifecycle.
**Reference tests/metrics.test.ts**
```ts
import {
clearServices,
unregisterService,
} from "@tavojs/core/dev";
import {
metricsKey,
installMetricsService,
} from "../src/services/metrics";
export function beforeEachServiceTest() {
clearServices();
installMetricsService();
}
export function afterEachServiceTest() {
unregisterService(metricsKey);
}
```
## Treat the registry as process-wide during SSR
The service registry lives on `globalThis`. A Node process can serve concurrent requests through the same service instance, so registration during SSR emits a warning that asks you to audit the lifetime.
- A shared service may be immutable or internally concurrency-safe, but it must not store the current request identity.
- Never attach a user, tenant, session, token, cart, permissions, or request headers to a registered service.
- Pass request values into stateless service methods, or create the request-bound client inside the loader/action/middleware that owns it.
- Do not silence a duplicate or SSR warning with override until the replacement and lifetime are intentional.
**A typed singleton is still a singleton**
createServiceKey improves type safety; it does not make a service request-scoped. Authentication and authorization must remain in the server request path.
## Distinguish services from capabilities
Controllers expose both `this.services` and `this.capabilities`. Services are registered directly by the application. Capabilities are contracts contributed by installed plugins and resolved through the active Pages runtime.
- Use `capabilities.resolve` for a required plugin capability and `tryResolve` for an optional one.
- Required capability resolution without an active Pages runtime fails with `TAVO_PLUGIN_004`.
- Keep plugin ownership, permissions, and replacement rules in the Plugin API v1 manifest instead of recreating them in the service registry.
## Verify registration and isolation
- Resolve the typed key after installation and assert the same instance is returned.
- Verify get throws and `tryGet` returns undefined before installation.
- Register a duplicate in a focused test and confirm intentional override behavior.
- Clear services between tests that share a process.
- Run concurrent SSR requests and confirm no request-specific value is retained by a service.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Async ownership and cancellation
> Choose route loaders, resources, actions, forms, Deferred boundaries, and controller actions by lifetime and cancellation behavior.
Canonical page: https://tavojs.dev/docs/core/async-ownership
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- An asynchronous read, mutation, component task, or streamed value.
## Outcomes
- Assign each asynchronous task to one clear owner.
- Propagate AbortSignal and prevent stale completions from publishing.
- Distinguish abort, failure, reset, and committed side effects.
## Choose the owner before the primitive
- Use a route load export when navigation cannot render the route correctly without the data.
- Use [`createResource`](/docs/core/api/data-actions-and-async#api-tavojs-core--createresource "View createResource in the Core API reference") when one mounted feature owns independently refreshable read data.
- Use a route action for a server mutation and its HTTP response.
- Use [`createAction`](/docs/core/api/data-actions-and-async#api-tavojs-core--createaction "View createAction in the Core API reference") or [`createFormAction`](/docs/core/api/data-actions-and-async#api-tavojs-core--createformaction "View createFormAction in the Core API reference") for observable client mutation state.
- Use [`TavoController`](/docs/core/api/application#api-tavojs-core--tavocontroller "View TavoController in the Core API reference").action for controller-owned work whose pending/result/error state should rerender that component.
- Use [`Deferred`](/docs/core/api/data-actions-and-async#api-tavojs-core--deferred "View Deferred in the Core API reference") when the server can send meaningful fallback HTML before a secondary value resolves.
## Give a component resource explicit cleanup
- load always starts a new operation and aborts the previous one. preload deduplicates the current pending operation.
- Forward the supplied signal to fetch and every abort-aware dependency.
- A stale completion cannot overwrite a newer load.
- An abort resolves the resource back to idle, clears error and `updatedAt`, and preserves the last data. reset also clears data.
- [`Resource`](/docs/core/api/data-actions-and-async#api-tavojs-core--resource "View Resource in the Core API reference") failures become error state; load resolves to that state instead of rethrowing the loader error.
**Reference src/components/ActivityPanel.tsx**
```tsx
import {
createResource,
createTavo,
TavoController,
} from "@tavojs/core";
type Activity = {
id: string;
summary: string;
};
class ActivityController extends TavoController {
activity = createResource(async ({ signal }) => {
const response = await fetch("/api/activity", { signal });
if (!response.ok) {
throw new Error("Activity could not be loaded.");
}
return response.json();
});
onMount() {
this.listen(this.activity.store, () => {
this.model.patch({});
});
this.cleanup(() => {
this.activity.abort("Activity panel unmounted.");
});
void this.activity.load();
}
reload() {
void this.activity.load();
}
}
export const ActivityPanel = createTavo({
controller: ActivityController,
view({ controller }) {
const activity = controller?.activity.read();
if (!activity || activity.status === "idle") {
return
Activity is idle.
;
}
if (activity.status === "loading") {
return
Loading activity…
;
}
if (activity.status === "error") {
return
Activity failed to load.
;
}
return (
{activity.data?.map((item) => {
return
{item.summary}
;
})}
);
},
});
```
## Know whether a mutation rejects
The two client action primitives intentionally expose different caller behavior. Choose based on who owns control flow, then handle both the observable state and returned promise.
- `createAction.run` resolves to `ActionState` on success or handler failure. The failure is stored with status error.
- `TavoController.action.run` resolves with the handler result and rethrows a caught error while also exposing reactive error state.
- `createFormAction` mirrors `createAction` state and also records submitted values.
- Starting a newer run prevents an older completion from replacing the latest observable state.
- reset invalidates an in-flight completion and clears the complete state.
**Cancellation cannot undo a committed mutation**
AbortSignal stops waiting and can prevent later publication, but a database, payment, email, or remote API may already have committed. Use authorization, idempotency keys, and a server-side transaction where the business operation requires them.
## Handle controller action rejection
- A controller action is safe to create as a class field.
- Its pending, result, and error transitions rerender the owning `createTavo` component.
- Catch or await run from event handlers so a handled UI failure does not become an unhandled rejection.
**Reference Reference snippet**
```tsx
class SaveController extends TavoController {
save = this.action(async (name: string) => {
const response = await fetch("/projects", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name }),
});
if (!response.ok) {
throw new Error("Project could not be saved.");
}
return response.json() as Promise<{ id: string }>;
});
submit(name: string) {
void this.save.run(name).catch(() => {
// The view renders save.error.
});
}
}
```
## Propagate cancellation into Deferred work
[`Deferred`](/docs/core/api/data-actions-and-async#api-tavojs-core--deferred "View Deferred in the Core API reference") owns rendering of a promise-backed value; it does not create the underlying request. Pass one `AbortSignal` through `createDeferredValue` or [`Deferred`](/docs/core/api/data-actions-and-async#api-tavojs-core--deferred "View Deferred in the Core API reference") and into the operation that produces the promise.
- Give every [`Deferred`](/docs/core/api/data-actions-and-async#api-tavojs-core--deferred "View Deferred in the Core API reference") value a stable id when the server result should be serialized and reused during hydration.
- Use `timeoutFallback` for the typed `TAVO_DEFERRED_TIMEOUT` case and `errorFallback` for other rejection.
- A pure CSR document renders fallback for a promise-backed [`Deferred`](/docs/core/api/data-actions-and-async#api-tavojs-core--deferred "View Deferred in the Core API reference") value; progressive patching is an SSR capability.
- Do not share request-specific promises through module variables or process-wide registries.
## Test race, abort, and cleanup paths
- Hold two operations pending, resolve the newer one first, and assert the older completion cannot replace state.
- Abort before start, while pending, and during component unmount.
- Assert the semantic difference between abort and reset, especially whether previous data remains.
- Reject every action/resource path and verify both observable state and promise behavior.
- For server mutations, retry with the same idempotency key and verify one committed effect.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# SSR and hydration
> Choose SSR, CSR, SSG, or revalidated output and keep the initial client tree consistent with server HTML.
Canonical page: https://tavojs.dev/docs/core/ssr-and-hydration
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Choose SSR, CSR, SSG, or revalidated output and keep the initial client tree consistent with server HTML.
## Choose rendering per route
The same page and component code can render on the server or client. SSR is the default in SSR development, preview, and the generated Node server. Use CSR only when the route depends on browser-only behavior and its initial HTML is not important.
- SSR renders for each request.
- CSR sends the document shell and resolves the route in the browser.
- SSG prerenders static routes during build.
- ISR caches SSR output and refreshes it after a revalidation interval.
## Configure route output
Static routes may provide `generateStaticParams` for dynamic paths. Revalidated output uses a runtime process-local cache by default, while Cookie or Authorization requests bypass static caching.
**Create src/pages/blog/[id].tsx — create this static route**
```tsx
import { notFound, type PageLoadContext, type PageProps } from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
const posts: Record = {
hello: { title: "Hello from Tavo.js" },
"release-notes": { title: "Release notes" }
};
export const prerender = true;
export const generateStaticParams = () => [{ id: "hello" }, { id: "release-notes" }];
export const load = ({ params }: PageLoadContext) => posts[params.id] ?? notFound();
export default function BlogPost({ data }: PageProps<{ title: string }>) {
return {data?.title};
}
```
## Keep hydration deterministic
`Tavo.js` serializes resolved route data so the browser can hydrate against the same route tree without immediately loading it again. Hydration warnings mean server and client produced different initial output.
- Do not read browser-only globals during the initial render without a guard.
- Use deterministic IDs from controller helpers.
- Avoid time, randomness, and locale differences between server and client output.
- Test the production SSR build, not only development mode.
## Rendering mode reference
Every route resolves to either SSR or CSR. SSR is the default. Static generation and revalidation are cache policies applied to SSR routes; they are not separate component runtimes.
**Reference src/pages/account.tsx**
```tsx
// Browser-rendered route with a useful server shell.
export const render = "csr";
export default function AccountPage() {
return Account settings;
}
```
API / option
Type
Default
Behavior
[`render`](/docs/core/api/components-and-dom#api-tavojs-core--render)
`"csr"`
SSR
Export render = "csr" to opt the route subtree out of server body rendering.
`prerender`
`boolean`
false
Marks a functional SSR route for build-time prerendering or runtime static caching. false disables an inherited static policy.
`revalidate`
`number | false`
unset
Enables static SSR caching for the given number of seconds. Values are floored and clamped to zero; false disables inherited caching.
`generateStaticParams`
`() => params[] | Promise`
unset
Lists build-time paths for a dynamic static route.
`csrFallback`
[`Child`](/docs/core/api/components-and-dom#api-tavojs-core--child) `|` `(``context``)` `=>` [`Child`](/docs/core/api/components-and-dom#api-tavojs-core--child)
empty route node
Provides meaningful server-shell content for a CSR route without executing its client loader.
**CSR ignores static-only exports**
On a CSR route, Tavo.js ignores static, revalidate, vary, and generateStaticParams. A dynamic head function also runs only when the browser resolves the route, so it cannot change the initial response. The route manifest reports these combinations as diagnostics.
Layout and page cache settings compose from root to leaf. Vary headers are trimmed, lowercased, and merged. Static cache tags are merged. If several layers provide numeric revalidation intervals, the shortest interval wins.
## Boot and hydration reference
[](/docs/core/api/application#api-tavojs-core--boottavo "View bootTavo in the Core API reference")detects server, hydrated SSR, and browser-only documents. It returns a discriminated result so application entrypoints can inspect what actually started.
API / option
Type
Default
Behavior
`root`
`Element | null`
unset
Uses a specific client mount element.
`rootSelector`
`string`
"#app"
Locates the client root when root is not provided.
`hydrate`
`boolean`
detected
Defaults to true when \_\_`TAVO_SSR__` or \_\_`TAVO_STATE__` exists. A root marked `data-tavo-render-mode`\="csr" always renders instead.
`serverFile`
`string`
"server.mjs"
Server boot returns none when this file is absent.
`modules / node.modules`
`PageModules`
required on server
Supplies the server route module map. Missing modules throw `TAVO_PAGES_005`.
[`getTavoBootMode`](/docs/core/api/application#api-tavojs-core--gettavobootmode)
`() => "server" | "ssr" | "csr" | "none"`
—
Reports the planned mode without starting the app.
**Reference Reference snippet**
```ts
import { bootTavo, getTavoBootMode } from "@tavojs/core";
console.log(getTavoBootMode());
const result = await bootTavo();
if (result.mode === "client") {
// result.root exposes render, hydrate, and unmount.
}
```
- A missing client root throws `TAVO_PAGES_002` and names the expected selector.
- Hydration restores serialized page, layout, and store state instead of rerunning the initial loaders.
- CSR boot resolves the initial loaders and same-origin redirects before mounting the route.
- Initial redirect resolution stops after eight redirects and warns about a likely middleware loop.
## Server rendering methods
**Reference Reference snippet**
```tsx
import {
renderDocument,
renderDocumentStream,
} from "@tavojs/core/server";
const html = renderDocument(, {
title: "Dashboard",
initialState: { locale: "en" },
});
const stream = renderDocumentStream(, {
title: "Dashboard",
});
```
API / option
Type
Default
Behavior
[`renderDocument`](/docs/core/api/server#api-tavojs-core-server--renderdocument)`(``node``,` `options``)`
`string`
—
Renders the complete HTML document and serialized initial state.
[`renderDocumentStream`](/docs/core/api/server#api-tavojs-core-server--renderdocumentstream)`(``node``,` `options``)`
`ReadableStream`
—
Streams the shell followed by deferred patch chunks.
[`createPagesRuntimeAsync`](/docs/core/api/server#api-tavojs-core-server--createpagesruntimeasync)`(``modules``,` `options``)`
`Promise`
—
Creates the technical server runtime used by generated CLI SSR templates.
[`renderPagesResponseFromRuntimeAsync`](/docs/core/api/server#api-tavojs-core-server--renderpagesresponsefromruntimeasync)`(``runtime``,` `pathname``,` `options``)`
`Promise`
—
Renders a response from that server runtime. Import it only from `@tavojs/core/server`.
**Use handlers for applications**
[](/docs/core/api/server#api-tavojs-core-server--createnoderequesthandler "View createNodeRequestHandler in the Core API reference")adds actions, plugins, image optimization, security headers, static response caching, and contained HTTP failures. Low-level render functions are intended for custom Node integrations.
## Static cache and invalidation contract
Tavo.js keeps a resolved route-data cache and a rendered-response cache. Both default to 1,024 process-local entries and evict the oldest entry when full. Set maxResolvedCacheEntries to 0 to disable route-data reuse, or provide a custom [`staticCache`](/docs/core/api/server#api-tavojs-core-server--ssrstaticcache "View SsrStaticCache in the Core API reference") for shared rendered output.
- Cache keys include request origin, pathname, query string, and declared vary headers.
- Localized routes also vary by Accept-Language automatically.
- Requests with Cookie or Authorization bypass shared static response caching and do not evict an existing public entry.
- Redirects and responses with status 500 or greater are not stored.
- Concurrent public renders for the same cache key share one in-flight render.
- Cache adapter read, write, and delete failures degrade to an uncached response instead of failing SSR.
**Reference Reference snippet**
```ts
const handler = createNodeRequestHandler({ modules, staticCache });
await handler.invalidateCache("post:hello");
await handler.invalidateCache(["posts", "homepage"]);
await handler.clearCache();
```
For process-local caching, use [](/docs/core/api/server#api-tavojs-core-server--creatememorystaticcache "View createMemoryStaticCache in the Core API reference"). A custom [](/docs/core/api/server#api-tavojs-core-server--ssrstaticcache "View SsrStaticCache in the Core API reference")adapter has the following complete contract, including optional tag invalidation and clearing:
**Reference Reference snippet**
```ts
import type {
SsrStaticCache,
SsrStaticCacheEntry,
} from "@tavojs/core/server";
export function createInspectableStaticCache(): SsrStaticCache {
const entries = new Map();
return {
get(key) {
return entries.get(key) ?? null;
},
set(key, entry) {
entries.set(key, entry);
},
delete(key) {
entries.delete(key);
},
invalidateTags(tags) {
const requested = new Set(tags);
let deleted = 0;
for (const [key, entry] of entries) {
if (!entry.tags.some((tag) => requested.has(tag))) continue;
entries.delete(key);
deleted += 1;
}
return deleted;
},
clear() {
entries.clear();
},
};
}
```
invalidateCache removes matching loader-resolution entries and rendered responses. clearCache clears both layers. A custom cache can implement invalidateTags and clear; otherwise the handler deletes the entries it has observed in the current process.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Rendering, head, and hydration
> Choose SSR or CSR, compose escaped route metadata, understand head cleanup, and keep private values out of browser-readable hydration state.
Canonical page: https://tavojs.dev/docs/core/rendering-head-and-hydration
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: server, browser, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A route with functional page and layout modules.
- Understanding of server rendering and browser hydration.
## Outcomes
- Choose a rendering mode and predict where loaders and head functions run.
- Compose route metadata without introducing HTML injection.
- Audit every value serialized into the hydration document.
## Treat SSR as the default
The route chain resolves to one [`PageRenderMode`](/docs/core/api/router#api-tavojs-core-router--pagerendermode "View PageRenderMode in the Core API reference"). Omitting the named render export selects SSR; only [`render`](/docs/core/api/components-and-dom#api-tavojs-core--render) `=` `"csr"` selects client-only route rendering.
Mode
Route declaration
Server
Browser
SSR
`No render export`
Middleware, eligible loaders, head, layouts, and the page render for the request.
The same tree hydrates, then later navigation resolves in the browser.
CSR
`export const render = "csr"`
Tavo.js renders the configured CSR fallback and static head contributions only.
The route resolves and renders after client boot.
- A CSR selection anywhere in the route module chain makes the resolved route CSR.
- Static generation, revalidation, vary, cache tags, and static params are incompatible with CSR and are ignored with manifest diagnostics.
- Use CSR only when the route cannot produce useful request HTML. Client interactivity does not require CSR; SSR pages hydrate into interactive components.
- Dynamic head functions do not run on the server for CSR routes because route data is not resolved there.
## Return escaped TSX from head
A named head export returns [`PageHeadExport`](/docs/core/api/router#api-tavojs-core-router--pageheadexport "View PageHeadExport in the Core API reference"): escaped TSX for normal metadata or a [`PageHead`](/docs/core/api/router#api-tavojs-core-router--pagehead "View PageHead in the Core API reference") object for response status and document attributes.
**Reference src/pages/projects/[id].tsx — named head export**
```tsx
import { Seo } from "@tavojs/core";
type Project = {
name: string;
summary: string;
};
export function head({
data
}: {
data: Project | null;
}) {
const title = data ? `${data.name} · Projects` : "Project";
return (
<>
{title}
>
);
}
```
- TSX children and attributes are escaped by the renderer. Prefer TSX and [`Seo`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--seo "View Seo in the Core API reference") for every normal metadata contribution.
- The hard raw-string boundary is `unsafeHeadHtml` in a `PageHead` object or [`Head`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--head "View Head in the Core API reference") component. There is no raw head string alias.
- `unsafeHeadHtml` is inserted without escaping. Never concatenate user, loader, request, database, translation, or CMS values into it unless a trusted sanitizer establishes the complete HTML policy.
- head may also return title, status, `htmlAttributes`, and `bodyAttributes` in a `PageHead` object.
**The unsafe name is the security review**
Treat every [`PageHead`](/docs/core/api/router#api-tavojs-core-router--pagehead)`.``unsafeHeadHtml` occurrence like an HTML injection sink. Require a narrow owner, a documented sanitizer or trusted constant, and a test that prevents request data from reaching it.
## Predict head precedence and browser cleanup
- [`Head`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--head "View Head in the Core API reference") contributions resolve from outer layouts to inner layouts, then the page.
- A dynamic layout head receives that layout's own loader data and error. The page head receives page data and the page-loader error.
- Later title, status, `htmlAttributes`, and `bodyAttributes` values win for the same field.
- Managed [`Seo`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--seo "View Seo in the Core API reference") entries are deduplicated by their framework key with the later value winning. Unkeyed head nodes preserve contribution order.
- On browser navigation, `Tavo.js` removes nodes owned by the previous route, applies the next route's nodes and attributes, and restores the document fallback title when the next route has no title.
**Status belongs to the HTTP response**
On SSR, head.status becomes the route response status. Do not use a visual heading or client-only metadata update as a substitute for returning the correct server status.
## Assume hydration state is public
SSR sends enough state for the browser to adopt the server-rendered tree. Anyone who receives the HTML can read this serialized state, including values that are not visibly rendered. Its ordered route data entries describe the resolved layout and page layers.
State
Browser exposure
Page data
Serialized for the resolved page.
layers and layerData
Serialized layout and page loader results, both ordered and keyed.
Store snapshots
Serialized when included in the document hydration state.
Plugin state
Serialized when a plugin contributes hydration state.
Errors
Hydration error details are redacted to a generic internal-server message.
- Return the minimum browser-safe shape from loaders. Keep tokens, session internals, credentials, private profile fields, and database records on the server.
- Server-only execution does not make a returned loader value secret; SSR loader output can still be serialized.
- Review nested layout data as carefully as page data because `layerData` exposes successful results by ID.
- Redaction is a failure safeguard, not a reason to pass rich server exceptions into view props or custom error output.
## Keep the server and client trees compatible
- Render deterministic initial output from the same route data and serialized store state on both sides.
- Move DOM reads, browser storage, timers, observers, and subscriptions into client lifecycle hooks.
- Do not branch initial markup on `Date.now`(), `Math.random`(), locale defaults, viewport measurements, or undocumented globals.
- A hydration mismatch is a correctness failure: fix the divergent input instead of suppressing the warning.
- Clean up head ownership, controller work, and subscriptions when navigation replaces the route.
## Verify source HTML and hydrated behavior
**Run Terminal**
```bash
npx tavo inspect route /projects/example --json
npx tavo build
PORT=4174 node .tavo/build/server/start.mjs
```
- Inspect the raw document response to confirm SSR content, status, escaped metadata, and the absence of private values.
- Inspect the live head before and after client navigation to confirm stale title, meta, html attributes, and body attributes are removed or replaced.
- Hydrate with browser console errors treated as test failures.
- Test a CSR route separately: the raw response should contain the chosen fallback and only static head contributions.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Static output and route cache
> Use revalidated SSR, build-time parameter enumeration, vary keys, and cache tags while avoiding unresolved permanent-cache behavior.
Canonical page: https://tavojs.dev/docs/core/static-output-and-cache
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: build, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- An SSR route whose output is safe to share between requests.
- A deployment that serves Tavo.js's generated client and Node output.
## Outcomes
- Enable cached SSR with the minimum non-redundant declaration.
- Enumerate dynamic paths and compose inherited cache policy.
- Verify cache headers, variation, privacy bypasses, and tags.
## Choose one settled cache policy
**Reference src/pages/catalog.tsx**
```tsx
export const revalidate = 300;
export const cacheTags = ["catalog"];
export default function CatalogPage() {
return Catalog;
}
```
Intent
Functional module
[`defineRoutePage`](/docs/core/api/router#api-tavojs-core-router--defineroutepage "View defineRoutePage in the Core API reference")
Behavior
Cached SSR with regeneration
`export const revalidate = 300`
`revalidate: 300`
Numeric revalidate automatically enables static caching.
Build-time static HTML without revalidation
`export const prerender = true`
`static: true`
Use one form only; see the permanent-cache contract note below.
Disable inherited static policy
`export const revalidate = false`
`revalidate: false`
Resets inherited static and revalidation policy.
**Do not combine redundant static declarations**
A numeric revalidate already enables cached SSR, so prerender = true is redundant. In [`defineRoutePage`](/docs/core/api/router#api-tavojs-core-router--defineroutepage "View defineRoutePage in the Core API reference"), static: true is the helper equivalent of the named prerender export. A route that declares both static and prerender forms is rejected.
## Declare revalidated dynamic output
For a dynamic cached route, `generateStaticParams()` returns [`PageStaticParams`](/docs/core/api/router#api-tavojs-core-router--pagestaticparams "View PageStaticParams in the Core API reference") for the paths the build should materialize. The loader and cache-tag resolver receive [`PageLoadContext`](/docs/core/api/router#api-tavojs-core-router--pageloadcontext "View PageLoadContext in the Core API reference").
**Reference src/pages/catalog/[id].tsx**
```tsx
import type {
PageLoadContext,
PageProps
} from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Product = { id: string; name: string };
type ProductParams = { id: string };
export const revalidate = 300;
export const vary = "accept-language";
export function cacheTags({
params
}: PageLoadContext): string[] {
return ["catalog", `product:${params.id}`];
}
export function generateStaticParams(): ProductParams[] {
return [{ id: "starter" }, { id: "team" }];
}
export async function load({
params,
signal,
url
}: PageLoadContext): Promise {
const response = await fetch(
new URL(`/api/catalog/${params.id}`, url),
{ signal }
);
if (!response.ok) {
throw new Error("Could not load product");
}
return response.json() as Promise;
}
export default function ProductPage({
data
}: PageProps) {
return (
{data?.name}
);
}
```
- `generateStaticParams` returns parameter records for dynamic paths the build must enumerate. Every record must provide values expected by that route pattern.
- A numeric revalidate is measured in seconds, rounded down, and clamped to zero.
- Static policy composes through the route chain. The shortest finite revalidate value wins.
- vary names are lowercased and deduplicated. Localization also adds Accept-Language variation.
- CSR routes ignore static policy and static params with a manifest diagnostic.
## Understand Node cache behavior
Inspection exposes the composed [`PageCachePolicy`](/docs/core/api/router#api-tavojs-core-router--pagecachepolicy "View PageCachePolicy in the Core API reference"). Each static or request-aware tag declaration follows [`PageCacheTags`](/docs/core/api/router#api-tavojs-core-router--pagecachetags "View PageCacheTags in the Core API reference").
- A successful route with numeric revalidate emits Cache-Control: public, max-age=0, s-maxage=N.
- `Tavo.js` uses the static cache only for responses eligible under the composed route policy.
- Non-200 responses, resolved route errors, and requests with personal headers do not receive shared static cache headers.
- Vary values must cover every request header that can change shared output. Missing variation can serve one user's representation to another.
- Cache tags support targeted invalidation where the selected runtime exposes it. Attach tags to the page or documented directory layouts.
**Root cache tags are not yet a settled contract**
Current manifest composition does not reliably carry cacheTags from \_root into route policy. Do not depend on root-level tags; place them on a documented layout or page and verify the inspected route until Core closes this gap.
## Keep permanent static caching behind verification
The public intent of both prerender = true and static: true in [`defineRoutePage`](/docs/core/api/router#api-tavojs-core-router--defineroutepage "View defineRoutePage in the Core API reference") is build-time static HTML without revalidation. Current Core also gives a non-revalidated static route a long-lived immutable runtime cache response. That permanent runtime behavior is still under contract review.
- Use numeric revalidate when the Node runtime must refresh content; its cache behavior is explicit and settled.
- Use prerender or helper static only when build-time output is the intended source and the deployed asset/cache layer has been verified.
- Do not document the current immutable Node response as a permanent application guarantee until Core resolves the contract.
- A change to this behavior may require a Core migration note even if the authored route export stays the same.
## Cache only shareable successful output
- Do not statically cache a route whose output depends on a session, authorization decision, private cookie, or user-specific request header.
- A cache tag identifies related entries; it does not make private output safe to share.
- Return the correct non-200 status for failures and not-found output so they are not mistaken for a successful reusable document.
- Keep cache keys and tags bounded. Never place secret values in either one because operational tooling may expose them.
- When variation or privacy is uncertain, prefer uncached SSR and add caching only after request-level tests prove isolation.
## Inspect generation and response headers
**Run Terminal**
```bash
npx tavo inspect route /catalog/starter --json
npx tavo build
PORT=4174 node .tavo/build/server/start.mjs
curl -i http://127.0.0.1:4174/catalog/starter
```
- Confirm the inspected route has the expected static, revalidate, vary, and tag policy.
- Confirm every generated dynamic parameter produces output at the intended path.
- Assert Cache-Control and Vary for a successful anonymous request.
- Repeat with the deployment's personal headers and failure cases and confirm shared cache headers are absent.
- Exercise tag invalidation only through a runtime that explicitly exposes and documents it.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Streaming and async work
> Deliver the route shell first, defer secondary server content, and give every async operation an owner.
Canonical page: https://tavojs.dev/docs/core/streaming-and-async
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Deliver the route shell first, defer secondary server content, and give every async operation an owner.
## Defer secondary content
Route loaders should resolve data required for navigation, SEO, and the primary shell. [`Deferred`](/docs/core/api/data-actions-and-async#api-tavojs-core--deferred "View Deferred in the Core API reference") boundaries are for slower, optional server content such as analytics summaries, recommendations, or below-the-fold panels.
**SSR-only progression**
Promise-backed Deferred boundaries progressively patch an SSR stream. For browser-only async work, use route loaders, resources, controllers, or stores.
## Create a meaningful boundary
Give each boundary a stable ID, a lightweight fallback, and timeout behavior when the content is optional. Create the deferred value inside request-owned rendering work. A value created at module scope would be shared by every SSR request handled by that process.
**Create src/components/Stats.tsx — create this component**
```tsx
import { Deferred, createDeferredValue } from "@tavojs/core";
import { Card, Skeleton, Text } from "@tavojs/ui";
async function loadStats(): Promise<{ total: number }> {
await new Promise((resolve) => setTimeout(resolve, 250));
return { total: 12 };
}
export function Stats() {
const stats = createDeferredValue(loadStats(), {
id: "dashboard-stats",
timeoutMs: 1500
});
return }>
{(value) => {value.total} active projects}
;
}
```
## Give work an owner
Navigation owns loaders and middleware, a resource owns its current load, and deferred work belongs to its signal or render lifecycle. Pass `AbortSignal` through every supported layer and never publish results after the owner is gone.
- Treat `AbortError` as control flow, not a user-facing failure.
- Use transactions or idempotency keys for side effects; cancellation cannot undo a committed mutation.
- Set timeouts for optional remote dependencies that should not hold a stream open.
## Deferred API reference
**Reference Reference snippet**
```tsx
import {
createDeferredValue,
Deferred,
} from "@tavojs/core";
const recommendations = createDeferredValue(loadRecommendations(), {
id: "recommendations",
timeoutMs: 1500,
});
export function Recommendations() {
return (
Loading recommendations…
}
errorFallback={
Recommendations are unavailable.
}
>
{(items) => }
);
}
```
API / option
Type
Default
Behavior
`value`
`T` `|` `Promise``<``T``>` `|` [`DeferredValue`](/docs/core/api/data-actions-and-async#api-tavojs-core--deferredvalue)`<``T``>`
required
The immediate or deferred value rendered by the boundary.
`fallback`
[`Child`](/docs/core/api/components-and-dom#api-tavojs-core--child)
null
Initial SSR and pending content.
`errorFallback`
[`Child`](/docs/core/api/components-and-dom#api-tavojs-core--child) `|` `(``error``)` `=>` [`Child`](/docs/core/api/components-and-dom#api-tavojs-core--child)
fallback
Replaces the boundary when the promise rejects.
`timeoutFallback`
[`Child`](/docs/core/api/components-and-dom#api-tavojs-core--child) `|` `(``error``)` `=>` [`Child`](/docs/core/api/components-and-dom#api-tavojs-core--child)
errorFallback
Used specifically for `TAVO_DEFERRED_TIMEOUT`.
`id`
`string`
generated
Stable sharing and hydration key. Reusing an ID coordinates one promise across boundaries.
`timeoutMs`
`number`
disabled
Positive finite timeout in milliseconds; other values do not create a timer.
`signal`
`AbortSignal`
unset
Rejects pending work with the signal reason or `AbortError`.
`serialize / deserialize`
`functions`
identity
Controls the value stored in and restored from the hydration registry.
**Value options take precedence**
Options stored by createDeferredValue take precedence over the same props on Deferred. This lets a shared async unit own its ID, serialization, timeout, fallback, and signal once.
## SSR, CSR, and hydration behavior
API / option
Type
Default
Behavior
`SSR` `string` [`render`](/docs/core/api/components-and-dom#api-tavojs-core--render)
`HTML string`
fallback
Promise-backed boundaries render fallback content synchronously.
`SSR stream`
`HTML chunks`
stream: false
With streaming enabled, fallback arrives first and patch scripts follow as work settles.
`CSR`
`DOM` [`render`](/docs/core/api/components-and-dom#api-tavojs-core--render)
fallback
Promise-backed [`Deferred`](/docs/core/api/data-actions-and-async#api-tavojs-core--deferred "View Deferred in the Core API reference") does not coordinate browser data loading; use loaders, resources, controllers, or stores.
`Hydration`
`DOM hydrate`
reuse server state
Resolved, rejected, and timed-out streamed state is reused without restarting the client promise.
A timeout rejects with code TAVO\_DEFERRED\_TIMEOUT plus id, timeoutMs, and a safe message. Other rejections are serialized as a generic failure string; application error objects are not copied into the client document.
## Production streaming contract
**Reference Reference snippet**
```ts
createNodeRequestHandler({
modules,
stream: true,
document: { nonce: requestNonce }
});
```
- Streaming is disabled unless stream: true is passed to the handler.
- [`Deferred`](/docs/core/api/data-actions-and-async#api-tavojs-core--deferred "View Deferred in the Core API reference") patch scripts and serialized state receive `document.nonce` for a strict Content Security Policy.
- The Node handler waits for drain when response backpressure is signaled.
- A disconnected Node request aborts route work and cancels the stream reader.
- Redirects are returned as a complete one-chunk document with Location metadata.
- Use timeouts for optional dependencies so one remote service cannot hold the response open indefinitely.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Errors and code splitting
> Recover from component render failures, load component code on demand, and choose ErrorBoundary, lazy, or Deferred correctly.
Canonical page: https://tavojs.dev/docs/core/errors-and-code-splitting
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A component subtree that may fail while rendering or load code asynchronously.
## Outcomes
- Catch descendant render errors with an explicit recovery boundary.
- Load a component implementation lazily in browser and server workflows.
- Choose code loading separately from data streaming.
## Choose the boundary by what is pending
- Use a page error export for a contextual route-loader failure.
- Use [`ErrorBoundary`](/docs/core/api/errors-and-code-splitting#api-tavojs-core--errorboundary "View ErrorBoundary in the Core API reference") for a descendant component that throws while rendering.
- Use [`lazy`](/docs/core/api/errors-and-code-splitting#api-tavojs-core--lazy "View lazy in the Core API reference") when the component implementation should come from a dynamic import.
- Use [`Deferred`](/docs/core/api/data-actions-and-async#api-tavojs-core--deferred "View Deferred in the Core API reference") or [`createDeferredValue`](/docs/core/api/data-actions-and-async#api-tavojs-core--createdeferredvalue "View createDeferredValue in the Core API reference") when a promise-backed data value should render fallback UI and participate in progressive SSR.
- `Tavo.js` does not publish a Suspense component. Use the primitive that owns the actual failure or asynchronous work.
## Recover from descendant render errors
[`ErrorBoundary`](/docs/core/api/errors-and-code-splitting#api-tavojs-core--errorboundary "View ErrorBoundary in the Core API reference") renders its children until a descendant render throws. It then renders a static fallback or calls a fallback function with the error. Static and progressive server rendering apply the same fallback contract.
- Changing `resetKey` under `Object.is` clears a captured client error and retries the current children.
- Changing `resetKey` does not fix the underlying state and does not reset an unrelated lazy-loader cache.
- If rendering the fallback also fails, the error continues to the parent boundary or runtime error reporting.
- A boundary is not a replacement for route error exports, rejected action state, or expected form validation.
- Fallback UI should be accessible, concise, and offer only recovery that can actually change the failing condition.
**Reference src/components/ProjectSummary.tsx**
```tsx
import { ErrorBoundary } from "@tavojs/core";
function ProjectSummary({
project,
}: {
project: { name: string } | null;
}) {
if (!project) {
throw new Error("Project data is unavailable.");
}
return
;
}}
>
);
}
```
## Load component code on demand
[`lazy`](/docs/core/api/errors-and-code-splitting#api-tavojs-core--lazy "View lazy in the Core API reference") accepts a loader that resolves either a component or a module with a default component. Browser rendering starts one shared pending load, shows fallback UI, and rerenders mounted subscribers when the loader settles.
- fallback receives idle or loading. `errorFallback` receives error status and the caught loader error.
- If `errorFallback` is omitted, a failed load is thrown during the next render so the nearest `ErrorBoundary` can capture it.
- preload deduplicates the active load and resolves to the loaded component. `getStatus` reports idle, loading, loaded, or error.
- Synchronous SSR does not start the loader; it renders fallback. Call preload before rendering when loaded server output is required.
- A successful load is cached on that lazy component definition. A failed definition stays in error state; create a new definition or reload the owning module for a real retry.
**Reference src/pages/reports.tsx**
```tsx
import { lazy } from "@tavojs/core";
import type { PageProps } from "@tavojs/core/router";
type ReportsData = {
points: number[];
};
const ReportsChart = lazy(
() => import("../components/ReportsChart"),
{
fallback: ({ status }) => {
return
);
}
```
**Code loading and data loading are separate**
lazy loads a component implementation. Keep route-critical data in load, independently refreshable component data in createResource, and streamed server data in Deferred.
## Preload only when the server needs loaded output
- Call preload from an explicit server preparation path before render, not from the component render function.
- When fallback HTML is acceptable, let SSR render it and allow the browser to start loading after hydration.
- Preloading changes code availability, not [`Deferred`](/docs/core/api/data-actions-and-async#api-tavojs-core--deferred "View Deferred in the Core API reference") data state or route-loader caching.
**Reference Reference snippet**
```tsx
const InvoicePreview = lazy(
() => import("../components/InvoicePreview"),
);
export async function prepareInvoicePreview() {
await InvoicePreview.preload();
}
export function InvoiceSection() {
return ;
}
```
## Verify every state and recovery path
- Render a throwing child on the server and in the browser; assert the boundary fallback receives the error.
- Change `resetKey` with corrected child inputs and confirm the subtree renders again.
- Hold a lazy loader pending and assert both idle/loading fallback behavior and final replacement.
- Reject a lazy loader with and without `errorFallback`; verify the local fallback or parent `ErrorBoundary` owns the error.
- Render lazy synchronously during SSR to confirm the loader is not called, then preload and confirm loaded output.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# SEO, assets, and styling
> Own route metadata, optimized media, fonts, scripts, and component styling without losing SSR safety.
Canonical page: https://tavojs.dev/docs/core/seo-assets-and-styling
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Own route metadata, optimized media, fonts, scripts, and component styling without losing SSR safety.
## Put metadata next to the route
Use a page head export when metadata belongs to one route. Use [`Seo`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--seo "View Seo in the Core API reference") for structured title, description, canonical URL, robots, Open Graph, Twitter, and theme-color values. Dynamic head functions can read route params and loader data.
**Merge src/pages/dashboard/index.tsx — merge this head export**
```tsx
import { Seo } from "@tavojs/core";
export const head = ;
```
## Use framework asset components
[`Image`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--image "View Image in the Core API reference") provides one API for CSR assets and optional SSR optimization. Optimized remote images require an explicit HTTPS host allowlist; local assets must remain inside the public directory.
- Install the optional sharp dependency only when the server performs image optimization.
- Mark only above-the-fold images as priority.
- Use the ?component query when an SVG should render inline and accept component props.
- Use [`Font`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--font "View Font in the Core API reference") and [`Script`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--script "View Script in the Core API reference") when loading behavior and document placement matter.
## Keep styling boundaries clear
Use CSS modules or `Tavo.js` UI component props for ordinary local presentation. The lower-level style registry is for libraries that must register deduplicated CSS during both SSR and browser rendering.
- Call style(id, css) while an SSR registry is active; repeated IDs are emitted once.
- Use `ensureClientStyle`(id, css) when a browser-only integration must install one managed style element.
- Keep IDs stable across server and browser rendering so hydration does not duplicate CSS.
**Reference src/server/render-badge.tsx**
```tsx
import {
createStyleRegistry,
renderStyleTags,
renderToString,
style,
withStyleRegistry,
} from "@tavojs/core";
function StatusBadge({ label }: { label: string }) {
style(
"status-badge",
".status-badge{border-radius:999px;padding:.25rem .5rem}"
);
return {label};
}
const registry = createStyleRegistry();
const body = withStyleRegistry(registry, () => {
return renderToString();
});
const styles = renderStyleTags(registry);
const html = `${styles}${body}`;
```
**Prefer application-level styling**
Do not build a parallel styling system for ordinary pages. Use this registry only when reusable framework-level code needs SSR collection and client deduplication.
## Head and SEO properties
- Use the route head export for route-owned metadata and [`Head`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--head "View Head in the Core API reference") for component-owned insertion. Raw HTML is accepted only through `unsafeHeadHtml`; there is no head string alias.
- An explicit robots string takes precedence over `noIndex` and `noFollow`. Arrays of keywords become a comma-separated meta value.
- SEO title, description, canonical, and Open Graph image provide fallbacks for corresponding Open Graph and Twitter fields.
- `unsafeHeadHtml` is not escaped. Never place user-controlled data in it.
**Reference Reference snippet**
```ts
type PageHead = {
title?: string;
unsafeHeadHtml?: string;
status?: number;
htmlAttributes?: Record;
bodyAttributes?: Record;
};
type HeadProps = {
title?: string;
unsafeHeadHtml?: string;
children?: Child;
};
type SeoProps = {
title?: string; description?: string; canonical?: string;
robots?: string; noIndex?: boolean; noFollow?: boolean;
keywords?: string | string[]; author?: string; themeColor?: string;
openGraph?: SeoOpenGraph; twitter?: SeoTwitter;
};
```
## Image properties and optimizer defaults
- Default candidate widths are 320, 640, 960, 1280, and 1600. A supplied width also adds its 2x candidate; values above 3840 or at most zero are removed.
- Quality defaults to 75 and is rounded and clamped from 1 to 100. Format defaults to webp and generated sizes defaults to 100vw.
- priority selects eager loading and high fetch priority. Other images default to lazy loading; decoding defaults to async.
- Set unoptimized to keep the original URL. Remote optimization requires an HTTPS allowlist; local sources must stay inside `publicDir`.
**Reference Reference snippet**
```tsx
```
**Remote images are untrusted input**
The optimizer rejects private hosts, revalidates redirects, and applies file limits. Keep remote patterns exact and do not enable insecure remote fetching without a controlled network boundary.
## Font and Script properties
- [`Font`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--font "View Font in the Core API reference") uses href for an external stylesheet or src plus family for a self-hosted @font-face. Self-hosted fonts preload by default; external stylesheet preload is opt-in.
- Self-hosted format is inferred from woff2, woff, ttf, or otf when type is omitted. `crossOrigin` defaults to anonymous for font preloads.
- [`Font`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--font "View Font in the Core API reference") variable must be a CSS custom property name. fallback is appended to the generated variable value.
- [`Script`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--script "View Script in the Core API reference") supports src, type, async, defer, module, `noModule`, preload, content, json, id, nonce, integrity, `crossOrigin`, `referrerPolicy`, and `fetchPriority`.
- A json value without src defaults to application/ld+json. Inline content escapes less-than signs and closing script sequences.
**Reference Reference snippet**
```tsx
```
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# SEO and document head
> Compose escaped route metadata across layouts and pages, understand managed SEO precedence, and update the client document safely.
Canonical page: https://tavojs.dev/docs/core/seo-and-head
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A page or layout that contributes route-owned metadata.
## Outcomes
- Choose route head, Seo, or component Head by ownership.
- Predict layout/page precedence and managed metadata replacement.
- Verify SSR and client-navigation cleanup without raw HTML.
## Choose metadata ownership
- Use a route head export for metadata owned by one page or layout.
- Return escaped TSX, including [`Seo`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--seo "View Seo in the Core API reference"), when possible.
- Use [`Seo`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--seo "View Seo in the Core API reference") for title, description, canonical URL, robots, keywords, author, theme color, Open Graph, and Twitter fields.
- Use the [`Head`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--head "View Head in the Core API reference") component for metadata owned by the mounted lifetime of a reusable component outside route head composition.
- Use `unsafeHeadHtml` only for a raw-string boundary that cannot be expressed as TSX. It is not escaped and must never contain user-controlled input.
## Return escaped TSX from a route
- A head function may read route params, loader data, the URL, headers, signal, layout data, and the route error.
- A title element is normalized into the document title; other escaped nodes become head contributions.
- `PageHead` objects may additionally set status, `htmlAttributes`, `bodyAttributes`, and `unsafeHeadHtml`.
- CSR-only routes cannot depend on a dynamic server head result; route inspection reports invalid combinations.
**Reference src/pages/projects/[id].tsx**
```tsx
import { Seo } from "@tavojs/core";
import type {
PageLoadContext,
PageProps,
} from "@tavojs/core/router";
type Project = {
id: string;
name: string;
summary: string;
};
export async function load({
params,
}: PageLoadContext): Promise {
return {
id: params.id ?? "",
name: `Project ${params.id}`,
summary: "A Tavo.js project.",
};
}
export function head({
data,
}: {
data: Project;
}) {
return (
);
}
export default function ProjectPage({
data,
}: PageProps) {
return
{data?.name}
;
}
```
## Predict layout and page precedence
[`Head`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--head "View Head in the Core API reference") layers merge from the root layout toward the page. Later scalar and attribute values override earlier defaults. Escaped nodes retain contribution order, while [`Seo`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--seo "View Seo in the Core API reference") fields use stable managed identities.
- The page title and status override layout values when present.
- `htmlAttributes` and `bodyAttributes` merge by attribute name, with the later layer winning.
- A later [`Seo`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--seo "View Seo in the Core API reference") value replaces an earlier managed value for the same standard meta, Open Graph property, Twitter field, or canonical link.
- An unkeyed raw TSX node remains an ordered contribution; `Tavo.js` does not silently deduplicate arbitrary duplicate meta tags.
- Multiple [`Seo`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--seo "View Seo in the Core API reference") declarations retain their position relative to surrounding raw nodes while their defined fields merge.
- An explicit nested Open Graph or Twitter field survives a later top-level fallback that does not replace that nested field.
**Reference src/pages/_layout.tsx**
```tsx
import { Seo } from "@tavojs/core";
import type { PropsWithChildren } from "@tavojs/core";
export function head() {
return (
);
}
export default function RootLayout({
children,
}: PropsWithChildren) {
return
{children}
;
}
```
**Reference src/pages/about.tsx**
```tsx
import { Seo } from "@tavojs/core";
export function head() {
return [
,
,
];
}
export default function AboutPage() {
return
About Acme
;
}
```
## Replace managed metadata during navigation
- Client navigation removes stale route-managed metadata before applying the resolved route head.
- When the next route contributes no title, `Tavo.js` restores the document title configured before route metadata was applied.
- [`Seo`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--seo "View Seo in the Core API reference") renders the same managed fields in SSR and client navigation, preventing standard metadata from accumulating across routes.
- [`Component`](/docs/core/api/components-and-dom#api-tavojs-core--component "View Component in the Core API reference") [`Head`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--head "View Head in the Core API reference") inserts its children for the mounted lifetime and removes those nodes during cleanup. A supplied title is restored when that [`Head`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--head "View Head in the Core API reference") owner is disposed.
## Understand Seo field fallbacks
- An explicit robots string takes precedence over `noIndex` and `noFollow`.
- Keyword arrays become one comma-separated meta value.
- Open Graph title and description fall back to top-level title and description; Open Graph URL falls back to canonical.
- Twitter title and description fall back to top-level values; Twitter image falls back to the Open Graph image.
- Empty optional values do not emit metadata.
## Keep raw HTML visibly unsafe
**Reference Reference snippet**
```tsx
export function head() {
return {
title: "Trusted vendor integration",
unsafeHeadHtml:
'',
};
}
```
**Prefer TSX first**
unsafeHeadHtml bypasses escaping. Do not interpolate route params, loader data, query values, user content, or third-party responses into it. Plugin raw head contributions additionally require the unsafeHeadHtml permission and reason.
## Verify SSR and navigation output
- Render the route through SSR and assert exactly one title and one expected managed tag per [`Seo`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--seo "View Seo in the Core API reference") field.
- Compose a layout default with a page override and confirm the page wins without losing unrelated nested metadata.
- Mix [`Seo`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--seo "View Seo in the Core API reference") and raw TSX nodes and verify their intended order.
- Navigate between routes and confirm stale description, canonical, robots, Open Graph, and Twitter fields are removed.
- Navigate to a route without a title and confirm the configured document title returns.
- Inspect the application root and confirm title, meta, and canonical nodes were hoisted into head rather than left in page content.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Localization
> Create locale-aware messages, detection, document direction, and localized application links.
Canonical page: https://tavojs.dev/docs/core/localization
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Create locale-aware messages, detection, document direction, and localized application links.
## Use one application i18n service
`createI18n` owns supported locales, nested messages, the current locale, and optional locale prefixes. Register the same service with the pages runtime so initial SSR, hydration, links, and later navigation agree on the active locale.
## Define locales and messages together
The default locale is also the fallback unless you configure another. Locale detection can use the path, cookie, and Accept-Language header.
**Create src/i18n.ts — create this localization service**
```ts
import { createI18n } from "@tavojs/core";
export const i18n = createI18n({
defaultLocale: "en",
routing: { enabled: true },
locales: {
en: { label: "English", dir: "ltr" },
es: { label: "Español", dir: "ltr" }
},
messages: {
en: { projects: { title: "Projects" } },
es: { projects: { title: "Proyectos" } }
}
});
```
## Keep server and browser locale state aligned
- Resolve the request locale before rendering route loaders and metadata.
- Use the `Tavo.js` [`Link`](/docs/core/api/router#api-tavojs-core-router--link "View Link in the Core API reference") integration so internal destinations receive the active locale prefix.
- Set document language and direction from the resolved locale.
- Do not read a browser-only locale during the first render of an SSR page.
## Localization options and defaults
- locale and `fallbackLocale` default to `defaultLocale`. Direction defaults to ltr.
- The locale cookie defaults to tavo\_locale and detection order defaults to path, cookie, then Accept-Language.
- The default locale has no URL prefix unless `defaultLocalePrefix` is always or `localizePath` requests `includeDefaultLocale`.
- The service registers as `tavo`:i18n by default so [`Link`](/docs/core/api/router#api-tavojs-core-router--link "View Link in the Core API reference") and the pages runtime can discover it. Set `serviceName`: false to avoid registration.
**Reference Reference snippet**
```ts
createI18n({
defaultLocale,
locale?,
fallbackLocale?,
messages,
locales?: { [locale]: { label?: string; dir?: "ltr" | "rtl" | "auto" } },
routing?: false | {
enabled?: boolean;
defaultLocalePrefix?: "always" | "never";
cookieName?: string;
detectFrom?: Array<"path" | "cookie" | "header">;
},
serviceName?: string | false,
onMissingKey?: ({ key, locale, fallbackLocale }) => string | void
});
```
## Localization service reference
- text and t are reactive when read during component rendering. t interpolates string parameters and stringifies non-string leaf values.
- `setLocale` persists by default when cookie detection is enabled; pass persist: false for a temporary selection.
- A missing key uses `onMissingKey` when supplied and otherwise returns the key itself.
- `defineMessages` marks the central catalog for build-time locale splitting. Generated locale chunks are applied automatically by the framework runtime.
**Reference Reference snippet**
```ts
i18n.locale; i18n.defaultLocale; i18n.fallbackLocale; i18n.locales; i18n.dir;
i18n.messages; i18n.text; i18n.store;
i18n.setLocale(locale, { persist?: boolean });
i18n.setMessages(locale, messages, { merge?: boolean });
i18n.getLocaleInfo(locale?);
i18n.detectLocale({ pathname?, request?, headers?, cookie? });
i18n.resolvePath(pathname);
i18n.localizePath(pathname, locale?, { includeDefaultLocale?: boolean });
i18n.setLocaleFromRequest(input?); i18n.setLocaleFromPath(pathname);
i18n["t"](key, params?);
i18n.subscribe(listener, { immediate?: boolean });
i18n.watchLocale(listener, { immediate?: boolean });
```
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Security
> Keep request data isolated, validate mutation origins, protect secrets, and harden production SSR boundaries.
Canonical page: https://tavojs.dev/docs/core/security
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Keep request data isolated, validate mutation origins, protect secrets, and harden production SSR boundaries.
## Understand the secure defaults
`Tavo.js` escapes TSX text and attributes, rejects unsafe URL protocols, validates action origins for unsafe methods, applies baseline SSR headers, blocks external redirects by default, and disables remote image optimization until hosts are allowlisted.
- Keep raw HTML escape hatches free of user input.
- Set `trustedHosts` and `canonicalOrigin` behind a reverse proxy.
- Tune `maxRequestBodyBytes` for mutation endpoints.
- Add a deployment-specific Content Security Policy at the edge or adapter.
## Keep authentication request-scoped
Read cookies and sessions inside server middleware, loaders, and actions. Return only safe user fields to the rendered page. The memory store below makes the example runnable in local development; replace it with a shared production session store before deploying multiple processes. Global stores, services, and module variables may be shared between concurrent SSR requests.
**Create src/server/sessions.ts — create this server-only session module**
```ts
import "@tavojs/core/server-only";
import { createSessionStorage } from "@tavojs/core/server";
const secret = process.env.SESSION_SECRET;
if (!secret) throw new Error("SESSION_SECRET is required");
export const sessions = createSessionStorage<{ userId?: string }>({
cookie: {
name: "__session",
secrets: [secret],
maxAge: 60 * 60 * 24 * 7,
sameSite: "lax"
}
});
```
**Merge src/pages/account/_layout.tsx — merge this loader export**
```ts
import { defineServerLoader } from "@tavojs/core/router";
export const load = defineServerLoader(async ({ request }) => {
const { sessions } = await import("../../server/sessions");
const session = await sessions.getSession(request);
const userId = session.get("userId");
return {
user: userId ? { id: userId, name: "Signed-in developer" } : null
};
});
```
## Review production boundaries
Security is a deployment property as well as a framework property. Recheck proxy headers, host validation, cookies, CSP, and secret injection in the actual hosting environment.
- [`Store`](/docs/core/api/application#api-tavojs-core--store "View Store in the Core API reference") signing keys and API secrets outside source control.
- Use server-only modules for databases, sessions, and private clients.
- Authenticate webhooks independently before disabling origin validation.
- Protect the monitor endpoint with `TAVO_MONITOR_TOKEN`.
- Allow only exact remote image hosts and paths that the product needs.
## Framework security defaults
- TSX text and attribute values are escaped. Unsafe attribute names and javascript-style URL protocols are rejected.
- External redirects are blocked by default. Validate any target before enabling `allowExternalRedirects`.
- Unsafe route actions and plugin handlers validate Origin by default. Node handlers also require a local or trusted inbound host.
- SSR HTML and optimized image responses include nosniff, strict-origin-when-cross-origin, a restrictive camera/microphone/geolocation policy, and SAMEORIGIN framing.
- Node mutation bodies are limited to 10 `MiB` by default. Tune `maxRequestBodyBytes` or use direct-to-storage uploads for large files.
- Remote image optimization is disabled until hosts are explicitly allowlisted. Private hosts, unsafe redirects, path escapes, and oversized inputs are rejected.
## Treat hydration state as browser-readable data
Successful page loader results, layout loader results, [`Store`](/docs/core/api/application#api-tavojs-core--store "View Store in the Core API reference") snapshots selected for hydration, and plugin hydration contributions are serialized into the HTML response so the browser can resume the same application state. Escaping protects the document from script injection; it does not make those values private.
- Return display DTOs with only the fields that the rendered interface needs.
- Never return session IDs, access tokens, signing secrets, database records with private columns, or authorization-only policy details.
- Keep private values in server loaders, actions, middleware, sessions, or request-scoped plugin resources and derive a separate browser-safe result.
- Inspect the production HTML and \_\_`TAVO_STATE__` payload during security review; do not rely only on what is visibly rendered.
**Server-loaded does not mean server-private**
If a value becomes page or layout data used for hydration, a browser user can read it even when no component prints it.
## Server-only module boundaries
Place databases, session storage, secrets, and private clients under src/server or import the server-only marker. Use `defineServerOnly` to add a runtime assertion around an exported function.
- The server-only marker has no runtime exports; the build guard uses the import boundary to keep the module out of client bundles.
- `defineServerOnly` throws if the wrapped function is called in a browser.
- A shared route module can dynamically import a server module from an action, server loader, or server middleware.
**Reference src/server/auth.ts**
```ts
import "@tavojs/core/server-only";
import { defineServerOnly } from "@tavojs/core/server";
export const getPrivateClient = defineServerOnly(() => createPrivateClient({
token: process.env.PRIVATE_API_TOKEN
}));
```
## Safe mutation and authentication order
- Enforce the expected content type and request body limit.
- Validate the payload shape, then authenticate and authorize the current request.
- Keep origin validation enabled for browser mutations. Authenticate webhooks with a signature before opting out.
- Use idempotency keys or a transaction for retries. `AbortSignal` cancellation cannot undo a committed side effect.
- Return only safe fields. Never serialize access tokens, session IDs, or private service responses into route data.
- Keep request identity out of global stores, plugin runtime stores or capabilities, application services, and module variables because server processes handle concurrent requests.
## Content Security Policy and raw content
`Tavo.js` does not set one universal Content Security Policy because allowed scripts, styles, images, fonts, and analytics differ by application. Add a policy at the deployment edge or adapter and test the production SSR response.
**Reference Reference snippet**
```text
Content-Security-Policy:
default-src 'self';
base-uri 'self';
object-src 'none';
frame-ancestors 'self';
img-src 'self' data:;
script-src 'self' 'nonce-{nonce}';
style-src 'self';
font-src 'self'
```
**Use one nonce through the render**
Progressive streaming patches and intentional inline Script content need the same request nonce passed through document rendering and allowed by script-src. Raw head HTML must never contain user input.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Sessions and authentication
> Store opaque signed sessions, rotate credentials, protect authentication boundaries, and keep request identity isolated during SSR.
Canonical page: https://tavojs.dev/docs/core/sessions-and-authentication
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application using SSR for protected requests.
- A server-side session store or a development-only memory store.
## Outcomes
- Create signed opaque sessions and rotate their identifiers.
- Authenticate and authorize every protected server request.
- Keep request identity out of process-wide state.
## Create session storage
`Tavo.js` stores only a signed opaque session ID in the cookie. [`Session`](/docs/core/api/server#api-tavojs-core-server--session "View Session in the Core API reference") data stays in a `SessionStore` such as a database or Redis adapter. The built-in memory store is bounded but process-local, so reserve it for tests and local development.
- `cookie.name` and at least one non-empty secret are required. Every secret must contain at least 32 UTF-8 bytes.
- Cookie defaults are Path=/, `HttpOnly` enabled, and `SameSite`\=Lax. Secure is inferred from an HTTPS request unless explicitly configured.
- Secrets are checked in array order and new cookies are signed with the first secret. Put the new secret first and retain old secrets during a rotation window.
**Reference src/server/sessions.ts**
```ts
import "@tavojs/core/server-only";
import { createSessionStorage } from "@tavojs/core/server";
export const sessions = createSessionStorage<{ userId?: string }>({
cookie: {
name: "__session",
secrets: [process.env.SESSION_SECRET!],
maxAge: 60 * 60 * 24 * 7,
sameSite: "lax"
},
store: databaseSessionStore
});
```
## Session and storage API
- `getSession` accepts a Request or an object with a request property. Missing, invalid, or expired cookies create a new empty session.
- rotate replaces the opaque ID at the next commit and deletes the old store entry. Use it after login or a privilege change.
- destroy marks the session for deletion. A later commit returns an expired cookie and removes the store entry.
- redirect commits the session, appends Set-Cookie, normalizes the Location target, and defaults to status 303.
**Reference Reference snippet**
```ts
type Session = {
readonly data: T;
readonly id: string;
readonly isNew: boolean;
readonly rotated: boolean;
readonly secure: boolean;
get(key): T[key] | undefined;
has(key): boolean;
set(key, value): void;
delete(key): void;
rotate(): void;
destroy(): void;
};
sessions.getSession(request): Promise>
sessions.commitSession(session, { maxAge? }): Promise
sessions.destroySession(session, { maxAge? }): Promise
sessions.redirect(to, session, init?): Promise
```
## Custom and memory stores
- The memory limit defaults to 10,000. Oldest entries are evicted when capacity is exceeded; expired entries are removed when read.
- Set `maxEntries` to zero to disable persistence. A negative, infinite, or non-numeric limit throws.
- commit `maxAge` overrides the cookie `maxAge` for that response. Expiry is stored alongside server data and serialized into the cookie.
- Production stores must apply expiration consistently and support every runtime instance that can receive the user's next request.
**Reference Reference snippet**
```ts
type SessionStore = {
get(id: string): MaybePromise<{ data: T; expiresAt: number | null } | null>;
set(id: string, entry: { data: T; expiresAt: number | null }): MaybePromise;
delete(id: string): MaybePromise;
};
const memory = createMemorySessionStore({ maxEntries: 10_000 });
memory.size();
```
## Authenticate every request
- Read authentication in server middleware, loaders, actions, or plugin handlers for every protected request.
- After verifying login, rotate the session ID, set the user ID, and return `sessions.redirect` so the cookie is committed.
- Send only safe profile fields into loader data. Never expose the session ID, cookie, signing secret, or access token.
- A client auth store may mirror safe user data after hydration, but the server must still authorize each request from the session backend.
**Reference Reference snippet**
```ts
export const middleware = defineServerMiddleware(async ({ request }) => {
const session = await sessions.getSession(request);
if (!session.get("userId")) return { redirect: "/login", status: 302 };
});
export const load = defineServerLoader(async ({ request }) => {
const session = await sessions.getSession(request);
const user = await findUser(session.get("userId"));
return { user: user ? { id: user.id, name: user.name } : null };
});
```
**Request-scoped means request-scoped**
Never write the current user, tenant, cart, token, or permissions to a global store, plugin store, service, or module variable during SSR. Those objects can be shared by concurrent requests.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Node runtime and production security
> Configure the generated Node server, handler, origin and host checks, document boundary, static cache, and remote image optimizer.
Canonical page: https://tavojs.dev/docs/core/node-runtime
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application using SSR, server loaders, actions, sessions, plugin endpoints, or image optimization.
## Outcomes
- Configure the stable Node handler through tavo.config.ts.
- Apply the correct mutation origin and inbound host policy.
- Allowlist remote images without enabling private-network access.
- Operate the generated production server with explicit environment values.
## Configure the stable server boundary
Place [](/docs/core/api/server#api-tavojs-core-server--nodehandleroptions "View NodeHandlerOptions in the Core API reference")under ssr in the one root tavo.config.ts. Tavo.js supplies route modules and top-level plugins to generated builds; applications configure the remaining handler behavior. Document fields follow [](/docs/core/api/server#api-tavojs-core-server--renderdocumentoptions "View RenderDocumentOptions in the Core API reference"), and image fields follow [](/docs/core/api/server#api-tavojs-core-server--imageoptimizeroptions "View ImageOptimizerOptions in the Core API reference").
- `RenderDocumentOptions` has title and `unsafeHeadHtml`. It has no raw `document.head` string alias.
- Prefer route head exports and escaped TSX metadata. `unsafeHeadHtml` is unescaped and must never contain user-controlled content.
- `canonicalOrigin` must contain only an HTTP(S) origin: no credentials, path, query, or hash.
- `maxRequestBodyBytes` defaults to 10 `MiB` for the Node handler.
- Cookie or Authorization requests bypass static response caching.
**Reference tavo.config.ts**
```ts
import { defineConfig } from "@tavojs/core/config";
const reviewedBootstrapHtml =
'';
export default defineConfig({
ssr: {
canonicalOrigin: "https://app.example.com",
trustedHosts: ["app.example.com"],
maxRequestBodyBytes: 10 * 1024 * 1024,
stream: true,
document: {
lang: "en",
title: "Acme",
unsafeHeadHtml: reviewedBootstrapHtml
},
images: {
allowRemote: true,
remotePatterns: [{
protocol: "https:",
hostname: "images.example.com",
pathname: "/media/**"
}]
}
}
});
```
## Know the production HTTP behavior
[](/docs/core/api/server#api-tavojs-core-server--createnoderequesthandler "View createNodeRequestHandler in the Core API reference")applies this contract to the generated Node server. Low-level document and page render functions remain available when a platform needs a custom server integration.
API / contract
Type / boundary
Default / result
Behavior
`GET / HEAD`
`page request`
render route
Resolves the route, plugins, images, static response cache, and optional streaming document.
`unsafe method + action`
`mutation`
action response
Validates host/origin and optional content type before running the route action.
`unsafe method + plugin endpoint`
`mutation`
plugin response
Runs matching server middleware and the most-specific endpoint with request-scoped disposal.
`non-page method without handler`
`HTTP failure`
405
Returns Allow: GET, HEAD and baseline security headers.
`body over limit`
`HTTP failure`
413
Rejects before an action or endpoint receives the body.
`uncaught handler failure`
`HTTP failure`
generic 500
Contains the exception and avoids returning private error details.
`client disconnect`
`cancellation`
AbortSignal
Aborts request-owned work and cancels an active streaming reader.
- HTML, actions, plugin responses, images, and generated static assets receive baseline nosniff, referrer, permissions, and framing headers where applicable.
- `Tavo.js` does not invent a universal Content Security Policy. Add a deployment-specific policy and pass one request nonce through intentional inline content.
- The handler exposes `invalidateCache`(tags) and `clearCache`() for rendered and resolved cache invalidation.
**Reference server.mts**
```ts
import { createServer } from "node:http";
import { createNodeRequestHandler } from "@tavojs/core/server";
import * as modules from "virtual:tavo-pages";
const handleRequest = createNodeRequestHandler({
modules,
trustedHosts: ["example.com"],
stream: true,
});
createServer(handleRequest).listen(3000);
```
## Apply the exact origin and Host policy
Origin validation applies to unsafe methods when `validateOrigin` is not false. A present Origin must equal the normalized request URL origin. Missing Origin is accepted by the Fetch handler; the Node handler still requires a local or configured trusted inbound host.
API / contract
Type / boundary
Default / result
Behavior
`Fetch + matching Origin`
`unsafe action / endpoint`
accepted
Origin exactly matches new URL(`request.url`).origin.
`Fetch + mismatched Origin`
`unsafe action / endpoint`
403
Rejected before application mutation code runs.
`Fetch + missing Origin`
`unsafe action / endpoint`
accepted
Support for non-browser clients; authentication and authorization remain required.
`Node + untrusted Host`
`unsafe action / endpoint`
403
Rejected even when Origin is missing or agrees with the forged Host.
`Node + local Host`
`localhost / loopback`
trusted
Localhost, 127.0.0.1, and loopback IPv6 are implicit.
`Node + trustedHosts`
`exact host or hostname`
accepted
A configured hostname also matches that hostname with an inbound port.
`Node + canonicalOrigin`
`reverse proxy`
public URL origin
Constructs request URLs from the public origin and adds its host and hostname to the trusted set.
`validateOrigin: false`
`explicit opt-out`
no origin check
Reserve for independently authenticated integrations such as signature-verified webhooks.
**Origin validation is not authorization**
A same-origin or Origin-less request can still belong to the wrong user. Authenticate the session, authorize the resource, validate the payload, and use idempotency or a transaction before committing a mutation.
## Allowlist remote images narrowly
Configure remote loading through [](/docs/core/api/server#api-tavojs-core-server--imageoptimizeroptions "View ImageOptimizerOptions in the Core API reference"). The optional resolveHostname hook is a platform adapter with the exact signature shown below.
API / contract
Type / boundary
Default / result
Behavior
`allowRemote`
`boolean`
false
Remote sources remain disabled until explicitly enabled.
`remotePatterns`
`string | { protocol, hostname, port, pathname }[]`
\[\]
Allows only matching remote origins and paths. Prefer HTTPS and the narrowest pathname.
`resolveHostname`
`(hostname: string) => Promise>`
Node DNS
Advanced test/platform hook; every returned address must pass public-network validation.
`allowInsecureRemote`
`boolean`
false
Relaxes HTTP/private-network protections. Use only inside a controlled network boundary.
`timeoutMs / maxBytes`
`number`
5000 / 10 MiB
Bounds remote fetch time and source bytes.
`maxConcurrentTransforms`
`number`
4
Limits active image transformations.
`maxPendingTransforms`
`number`
64
Excess queued transformations receive 503.
- The optimizer rejects private hostnames and private DNS results, and checks redirected locations again.
- Local absolute paths must remain inside `publicDir`.
- Set the [`Image`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--image "View Image in the Core API reference") component's unoptimized prop when the source should bypass the optimizer.
- Install the optional sharp peer only on servers that perform transformations.
## Operate the generated Node server
API / contract
Type / boundary
Default / result
Behavior
`NODE_ENV`
`environment`
production
Selects mode-specific server env loading before the built entry is imported.
`HOST`
`bind address`
127.0.0.1
Use 0.0.0.0 only when the container or platform must accept external connections.
`PORT`
`number`
4174
HTTP listen port.
`TAVO_MONITOR_TOKEN`
`secret`
monitor disabled
Enables /\_tavo/monitor with an exact Authorization: Bearer header. Unauthorized requests return 404.
`assets/*`
`fingerprinted static assets`
1 year immutable
Generated hashed assets receive long-lived caching.
`other client files`
`static files`
no-cache
HTML and non-fingerprinted files are revalidated.
- Deploy .`tavo`/build/client for fully client/static applications or run `.tavo/build/server/start.mjs` for request-time behavior.
- Plain `tavo` preview delegates to Vite preview. `tavo` preview `--ssr` checks for missing or stale output and runs `tavo` build first when required.
- Protect the external TLS, proxy header, CSP, secret injection, health-check, and process supervision boundaries on the hosting platform.
**Run Terminal**
```bash
npx tavo build
HOST=0.0.0.0 PORT=4174 node .tavo/build/server/start.mjs
```
## Verify the deployed boundary
- Exercise one SSR GET, one client navigation, one action, one plugin endpoint, one error response, and one remote image policy decision.
- Verify the public Origin observed behind the real reverse proxy.
- Confirm an untrusted Host and mismatched Origin receive 403 before mutation code runs.
- Confirm the monitor endpoint is hidden without the Bearer token.
**Run Terminal**
```bash
npx tavo check
npx tavo build --report-json
npx tavo preview --ssr
npx tavo monitor --url http://127.0.0.1:4174 --token "$TAVO_MONITOR_TOKEN" --once
```
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Plugins and localization
> Choose the focused guide for framework plugins or locale-aware application content.
Canonical page: https://tavojs.dev/docs/core/plugins-and-localization
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: concept
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Choose the focused guide for framework plugins or locale-aware application content.
## Choose the integration you need
Plugins and localization both participate in application setup, but they solve different problems. They now have separate guides so you can learn one contract at a time.
[
## Plugins
Declare framework-owned capabilities, routes, endpoints, middleware, document head entries, and build integrations.Read guide →
](/docs/core/plugins)[
## Localization
Create one i18n service for messages, locale detection, localized paths, and document direction.Read guide →
](/docs/core/localization)
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Plugins
> Declare framework integrations with an explicit manifest and lazy client, server, and build implementations.
Canonical page: https://tavojs.dev/docs/core/plugins
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Declare framework integrations with an explicit manifest and lazy client, server, and build implementations.
## Use a plugin for framework integration
Create a plugin when a package must contribute framework-owned behavior such as capabilities, stores, pages, endpoints, middleware, head entries, or build configuration. Keep ordinary feature code in pages, components, and server modules.
A plugin has an identity, a package version, a manifest of everything it may contribute, and lazy phase loaders. The manifest is the contract; a phase may implement only the declarations owned by that plugin.
**Request data needs request scope**
Runtime capabilities and stores can outlive one request during SSR. Resolve the current user, tenant, session, and token inside request-scoped capabilities, middleware, or endpoint handlers.
## Declare Plugin API v1 before implementing
Published plugins bake the literal `apiVersion`: 1 into their descriptor. Keep server implementation behind the server phase so client builds do not evaluate private dependencies.
**Create src/plugins/project-api.ts — create this plugin descriptor**
```ts
import {
definePlugin,
definePluginPhase
} from "@tavojs/core/plugin";
export const projectApi = definePlugin({
id: "@project/api",
version: "1.0.0",
apiVersion: 1,
manifest: {
endpoints: [{
id: "projects",
methods: ["GET"],
match: { kind: "exact", path: "/api/projects" }
}],
exposure: [{
target: "server",
from: "/api/projects",
to: "/api/projects",
reason: "Expose the project's public read endpoint."
}]
},
server: async () => definePluginPhase({
endpoints: {
projects: async () => {
const { listProjects } = await import("../server/projects");
return Response.json(await listProjects());
}
}
})
});
```
**Create src/server/projects.ts — create this server-only implementation**
```ts
import "@tavojs/core/server-only";
type Project = { id: string; name: string };
const projects: Project[] = [
{ id: "alpha", name: "Alpha" },
{ id: "beta", name: "Beta" }
];
export async function listProjects(): Promise {
return projects;
}
```
## Install it in application configuration
Add the plugin instance to the existing plugins array in `tavo.config.ts`. Preserve other plugins and configuration fields; array order is not a substitute for declared dependencies or middleware ordering.
**Merge tavo.config.ts — merge this plugin into the existing configuration**
```ts
import { defineConfig } from "@tavojs/core/config";
import { projectApi } from "./src/plugins/project-api";
export default defineConfig({
pagesDir: "src/pages",
cssEntries: ["src/styles.css"],
plugins: [projectApi]
});
```
## Plugin hooks and lifecycle
- id identifies the package contract; version is the plugin package version used for dependency checks.
- The manifest declares every capability, store, page, endpoint, middleware entry, head entry, build contribution, permission, and public exposure the plugin may own.
- Client, server, and build loaders are lazy boundaries. Put environment-specific imports inside the matching phase module.
- Use `definePluginFactory` when consumers configure typed plugin options and `definePluginPhase` to preserve literal implementation keys.
- Runtime stores and capabilities may be shared between SSR requests. Use request-scoped capabilities, middleware, or handlers for the current user, session, or token.
**Reference Reference snippet**
```ts
type TavoPlugin = {
id: string;
version: string;
apiVersion: 1;
manifest: TavoPluginManifest;
client?: () => MaybePromise;
server?: () => MaybePromise;
build?: () => MaybePromise;
};
```
## Manifest declarations and phase implementations
- Every implementation key must match an ID declared in the manifest. Missing, extra, or cross-plugin contributions become diagnostics.
- Endpoint manifests declare allowed methods, exact or subtree path matching, and optional origin-validation policy. Endpoint handlers return a Response.
- Plugin pages and endpoints are namespaced by default. A manifest exposure must deliberately map them to a public application URL.
- Middleware manifests declare server or page target, lifecycle stage, and before/after ownership constraints.
- [`Head`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--head "View Head in the Core API reference") manifests declare a stable key, singleton or multi cardinality, and whether unsafe HTML permission is required. A raw contribution must declare the `unsafeHeadHtml` permission with a reviewable reason.
- Capabilities explicitly declare runtime or request scope. Request-scoped factories receive the current Fetch Request.
**Reference Reference snippet**
```ts
type TavoPluginPhase = {
capabilities?: Record;
stores?: Record;
pages?: Record;
endpoints?: Record;
middleware?: Record;
head?: Record MaybePromise)>;
build?: { plugins?: Record };
setup?: (context: PluginResolveContext) => MaybePromise;
dispose?: () => MaybePromise;
};
```
## Compatibility, ordering, and diagnostics
- Published descriptors must bake in the literal `apiVersion`: 1. Application-local plugins may use `TAVO_PLUGIN_API_VERSION` because they are rebuilt with their host.
- `checkPluginCompatibility` accepts a minimal { id, `apiVersion` } descriptor and rejects missing or incompatible versions with `TAVO_PLUGIN_001` before loading any phase.
- Declare plugin dependencies by plugin ID, compatible package version, and any required capability tokens.
- Declare middleware ordering with before and after ownership IDs. Dependency and ordering cycles become diagnostics.
- Duplicate ownership, undeclared contributions, incompatible contracts, unapproved raw head HTML, and implicit route replacement are rejected or diagnosed.
- Install a default plugin with plugins: \[plugin\]. Repeated installations require a unique application-supplied `instanceId`.
- Use plugins: { use, overrides } only for owner-aware replacement.
- Plugin diagnostics use the severity field.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Plugin API v1 reference
> Author, install, inspect, and operate Plugin API v1 descriptors with explicit ownership, scope, authority, and failure contracts.
Canonical page: https://tavojs.dev/docs/core/plugin-api-v1
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A framework integration that must contribute routes, endpoints, middleware, capabilities, stores, document head entries, or build behavior.
- An understanding of request-scoped versus process-wide server state.
## Outcomes
- Declare a Plugin API v1 manifest and matching lazy phases.
- Choose safe runtime and request capability scopes.
- Install repeated instances and inspect their ownership graph.
- Interpret plugin diagnostics without executing invalid phase code.
## Declare identity before implementation
Import plugin authoring APIs from `@tavojs/core/plugin`. A descriptor identifies the package, publishes literal Plugin API version 1, and declares every resource it may own before any client, server, or build phase is loaded.
Every plugin descriptor must write `apiVersion`: 1 literally so compatibility is visible to tooling and reviewers. `TAVO_PLUGIN_API_VERSION` is useful for host-side compatibility checks, not as a replacement inside the descriptor.
API / contract
Type / boundary
Default / result
Behavior
`id`
`string`
required
Stable plugin identity used by dependencies, owners, exposure, and overrides.
`version`
`semver string`
required
Plugin package version checked against dependency ranges.
`apiVersion`
`1`
required
Incompatible or missing versions fail with `TAVO_PLUGIN_001` before a phase is loaded.
`manifest`
[`TavoPluginManifest`](/docs/core/api/plugin#api-tavojs-core-plugin--tavopluginmanifest)
required
Declares capabilities, stores, pages, endpoints, middleware, head entries, build contributions, permissions, and public exposure.
`client / server / build`
[`lazy`](/docs/core/api/errors-and-code-splitting#api-tavojs-core--lazy) `phase loaders`
omitted
Load environment-specific implementation only after graph preflight succeeds.
**Reference src/plugins/audit-log.ts**
```ts
import {
defineCapability,
definePlugin,
definePluginPhase
} from "@tavojs/core/plugin";
export type AuditLog = {
write(event: { name: string; actorId?: string }): Promise;
};
export const auditLog = defineCapability({
provider: "@acme/audit",
name: "audit-log",
scope: "runtime"
});
export const auditPlugin = definePlugin({
id: "@acme/audit",
version: "1.0.0",
apiVersion: 1,
manifest: {
provides: [auditLog]
},
server: async function loadAuditServerPhase() {
return definePluginPhase({
capabilities: {
"audit-log": function createAuditLog(): AuditLog {
return {
async write(event) {
await persistAuditEvent(event);
}
};
}
}
});
}
});
```
**The manifest is enforced**
A phase implementation key must match an ID owned by the manifest. Missing, extra, cross-plugin, or environment-inappropriate contributions become diagnostics instead of silently extending authority.
## Choose capability scope and dependency authority
Capabilities are owned tokens, not global service names. A runtime capability may be shared for the life of the plugin runtime. A request capability is created for one Fetch Request and disposed after its response body completes or is cancelled.
API / contract
Type / boundary
Default / result
Behavior
`runtime`
[`PluginCapabilityToken`](/docs/core/api/plugin#api-tavojs-core-plugin--plugincapabilitytoken)`<``T``,` `"runtime"``>`
one value per plugin runtime
Use for stateless clients, shared pools, clocks, and other concurrency-safe resources.
`request`
[`PluginCapabilityToken`](/docs/core/api/plugin#api-tavojs-core-plugin--plugincapabilitytoken)`<``T``,` `"request"``>`
one value per request scope
Use for the current request, tenant, authenticated identity, trace, or request-owned transaction.
`dependencies[].capabilities`
[`AnyPluginToken`](/docs/core/api/plugin#api-tavojs-core-plugin--anyplugintoken)`[``]`
no access
A consumer may resolve only tokens explicitly declared on its dependency.
`resolve / tryResolve`
`typed capability lookup`
throws / undefined
Resolution is owner-aware; `tryResolve` converts unavailable access to undefined.
- Never place the current user, session, token, tenant, or permissions in a runtime capability.
- Request-scoped resources stay alive while a streaming response is being read and dispose after completion or cancellation.
- Dependency and capability cycles fail graph validation; they are not resolved by array order.
- MVC controllers can resolve runtime tokens through `this.capabilities` while an active pages runtime is rendering.
**Reference src/plugins/audit-consumer.ts**
```ts
import {
definePlugin,
definePluginPhase
} from "@tavojs/core/plugin";
import { auditLog } from "./audit-log";
export const auditConsumer = definePlugin({
id: "@acme/audit-consumer",
version: "1.0.0",
apiVersion: 1,
manifest: {
dependencies: [{
id: "@acme/audit",
version: "^1.0.0",
capabilities: [auditLog]
}],
endpoints: [{
id: "record",
methods: ["POST"],
match: { kind: "exact", path: "/record" }
}]
},
server: async function loadConsumerServerPhase() {
return definePluginPhase({
endpoints: {
record: async function recordAuditEvent(context) {
const audit = await context.resolve(auditLog);
await audit.write({ name: "recorded" });
return Response.json({ ok: true });
}
}
});
}
});
```
## Declare store hydration explicitly
Plugin stores are runtime-scoped `Tavo.js` stores. Hydration is opt-in because serialized server state crosses into the browser. A hydrated store must provide validation, serialization, and deserialization together.
- hydrate defaults to false.
- hydrate: true without validate, serialize, and deserialize throws `TAVO_PLUGIN_002` during definition.
- Hydration payloads are keyed by plugin owner and store name, so named instances stay isolated.
- Invalid deserialized state is rejected instead of being installed into the store.
**Reference src/plugins/counter.ts**
```ts
import {
definePlugin,
definePluginPhase,
definePluginStore
} from "@tavojs/core/plugin";
type CounterState = { count: number };
export const counterStore = definePluginStore({
provider: "@acme/counter",
name: "counter",
hydrate: true,
serialize(value) {
return { count: String(value.count) };
},
deserialize(value) {
return {
count: Number((value as { count?: unknown }).count)
};
},
validate(value): value is CounterState {
return Boolean(
value &&
typeof value === "object" &&
Number.isFinite((value as { count?: unknown }).count)
);
}
});
export const counterPlugin = definePlugin({
id: "@acme/counter",
version: "1.0.0",
apiVersion: 1,
manifest: {
stores: [counterStore]
},
server: async function loadCounterServerPhase() {
return definePluginPhase({
stores: {
counter: function createCounterState(): CounterState {
return { count: 0 };
}
}
});
}
});
```
## Match every manifest contribution to its runtime phase
API / contract
Type / boundary
Default / result
Behavior
`pages`
`page phase`
namespaced
Plugin pages remain under their plugin namespace unless declared exposure maps them publicly.
`endpoints`
`server phase`
namespaced
Declare methods, exact or subtree matching, and origin validation. Exact and method-specific matches win deterministically.
`middleware`
`server or page phase`
declared stage
Use server:before-handler, page:before-app, or page:after-app plus explicit before/after owner constraints.
`head`
`client/server phase`
escaped TSX
Declare a stable key and singleton or multi cardinality. Raw strings require `unsafeHeadHtml` on the entry and a manifest permission with a reason.
`build`
`build phase`
none
Declare aliases, defines, and ordered build plugin IDs before implementing their values.
`setup / dispose`
`phase lifecycle`
omitted
Setup runs after successful initialization; dispose releases plugin-owned runtime resources in reverse lifecycle order.
- Framework paths under /\_tavo remain reserved even when an application remaps exposure.
- Endpoint, page, singleton head, alias, and define collisions fail unless an exact owner-aware override resolves them.
- Plugin endpoint handlers return a Fetch Response. Uncaught request or disposal failures use `TAVO_PLUGIN_009`.
- Raw head HTML must be declared twice: `unsafeHeadHtml` on the head entry and the `unsafeHeadHtml` permission with a reviewable reason.
**Reference src/plugin.ts**
```ts
import { definePlugin } from "@tavojs/core/plugin";
export default definePlugin({
id: "@example/audit",
apiVersion: 1,
version: "1.0.0",
manifest: {
endpoints: [{
id: "events",
methods: ["POST"],
match: { kind: "exact", path: "/events" }
}],
middleware: [{
id: "request-context",
target: "server",
stage: "server:before-handler"
}]
},
server: () => import("./server")
});
```
## Install defaults, named instances, and overrides
The top-level plugins field in `tavo.config.ts` accepts the `TavoPluginInput` union. Use a simple array for default installations. Use the { use, overrides } form for named instances, disabling an installation, exposure remapping, or explicit replacement.
- The owner of a default installation is plugin-id#default. A named installation uses plugin-id#`instanceId`.
- Installing the same plugin more than once without distinct `instanceId` values is fatal.
- enabled: false omits the installation and its manifest-declared permissions and exposure.
- expose remaps manifest-declared page or server exposure; it does not grant undeclared contributions.
- Override kinds are page, endpoint, head, alias, and define. Both the replaced owner and winning owner must match exactly.
**Reference tavo.config.ts**
```ts
import { defineConfig } from "@tavojs/core/config";
import { analyticsPlugin } from "./src/plugins/analytics";
import { dashboardPlugin } from "./src/plugins/dashboard";
export default defineConfig({
plugins: {
use: [
{
plugin: analyticsPlugin,
instanceId: "primary",
expose: {
server: {
from: "/",
to: "/analytics"
}
}
},
{
plugin: dashboardPlugin,
instanceId: "primary"
},
{
plugin: dashboardPlugin,
instanceId: "disabled-preview",
enabled: false
}
],
overrides: [{
kind: "page",
key: "/dashboard",
replace: {
plugin: "@acme/dashboard",
instanceId: "primary"
},
with: {
owner: "app"
}
}]
}
});
```
## Inspect before loading phases
Use the CLI inspection command for normal plugin verification. It reports the serializable preflight without presenting framework host compilation or runtime construction as plugin-author APIs. Experimental tooling that genuinely needs the graph can import [](/docs/core/api/dev#api-tavojs-core-dev--inspectplugingraph "View inspectPluginGraph in the Core API reference")from `@tavojs/core/dev`.
- Inspect owners, versions, dependencies, capabilities, mounts, middleware, endpoints, head keys, build values, permissions, exposure, and overrides.
- A diagnostic includes code, severity, phase, message, and optional resource, owners, and remediation hint.
- `TAVO_PLUGIN_001` rejects incompatible API versions before any phase load.
- `TAVO_PLUGIN_002` through 009 cover invalid identity/manifest, ownership, dependency, cycle, permission, phase, initialization/build, and request/disposal failures.
**Run Terminal**
```bash
npx tavo inspect plugins --json
npx tavo check
npx tavo verify --json
npx tavo build
```
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Testing, diagnostics, and observability
> Catch project-shape, type, route, hydration, performance, and production failures at the appropriate layer.
Canonical page: https://tavojs.dev/docs/core/testing-and-diagnostics
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Catch project-shape, type, route, hydration, performance, and production failures at the appropriate layer.
## Use the fastest useful check
During implementation, start with targeted tests and typechecking. Before shipping, validate the production route graph, SSR output, browser behavior, and route bundle sizes.
**Run Terminal — run from the project root**
```bash
npx tavo doctor
npx tavo check
npx tavo routes
npx tavo build --report-json
npx tavo preview --ssr
```
## Make runtime failures actionable
Development diagnostics can report runtime errors, mount and patch traces, and hydration mismatches with phase and DOM path context. Use this information to find the first server-client divergence rather than suppressing the warning.
**Create src/diagnostics.ts — create this development-only setup**
```ts
import { configureDevDiagnostics } from "@tavojs/core/dev";
configureDevDiagnostics({
enabled: true,
devMode: true,
onHydrationMismatch: (event) => console.warn(event.path, event.kind),
onError: (error) => console.error(error)
});
```
## Instrument lifecycle events without leaking data
Instrumentation emits route resolution, middleware, loader, action, and cache lifecycle events. Events contain route patterns and timing metadata rather than request bodies, cookies, tokens, or loader results.
**Production validation**
A passing dev server is not enough. Run the production build and SSR preview, then exercise navigation, actions, error routes, and hydration in a browser.
## Framework testing API
API / option
Type
Default
Behavior
[`createTestRoot`](/docs/core/api/dev#api-tavojs-core-dev--createtestroot)`(``)`
[`TestRoot`](/docs/core/api/dev#api-tavojs-core-dev--testroot)
new detached div
Requires a DOM environment and exposes render, hydrate, unmount, text, and html.
[`flushMicrotasks`](/docs/core/api/dev#api-tavojs-core-dev--flushmicrotasks)`(``)`
`Promise`
—
Waits through one queued microtask turn.
[`expectTextContent`](/docs/core/api/dev#api-tavojs-core-dev--expecttextcontent)`(``root``,` `expected``)`
`void`
—
Throws with expected and actual text when the values differ.
[`captureDiagnostics`](/docs/core/api/dev#api-tavojs-core-dev--capturediagnostics)`(``)`
`{ traces, mismatches, restore }`
diagnostics enabled
Captures trace and hydration mismatch events until restore disables and clears callbacks.
[`createPagesTestHarness`](/docs/core/api/dev#api-tavojs-core-dev--createpagestestharness)`(``modules``,` `options``)`
`route harness`
—
Exposes runtime, `renderPath`, and asynchronous `resolvePath`. Rendering requires a DOM.
[`clearServices`](/docs/core/api/dev#api-tavojs-core-dev--clearservices) `/` [`unregisterService`](/docs/core/api/dev#api-tavojs-core-dev--unregisterservice)
`test cleanup`
—
Resets process-wide service registration between tests.
**Reference Reference snippet**
```tsx
import {
captureDiagnostics,
createTestRoot,
expectTextContent,
flushMicrotasks
} from "@tavojs/core/dev";
const diagnostics = captureDiagnostics();
const root = createTestRoot();
root.render();
await flushMicrotasks();
expectTextContent(root, "Count: 0");
root.unmount();
diagnostics.restore();
```
## Coded framework errors
[](/docs/core/api/runtime-contracts#api-tavojs-core--tavoerror "View TavoError in the Core API reference")keeps a stable code for tools and logs while retaining a human message, optional details, remediation hint, and cause. Use [](/docs/core/api/runtime-contracts#api-tavojs-core--istavoerror "View isTavoError in the Core API reference")before reading the code and [](/docs/core/api/runtime-contracts#api-tavojs-core--formattavoerror "View formatTavoError in the Core API reference")when presenting the hint to a developer.
**Reference Reference snippet**
```ts
import { formatTavoError, isTavoError } from "@tavojs/core";
try {
await startApplication();
} catch (error) {
if (isTavoError(error)) console.error(error.code, formatTavoError(error));
}
```
- `TAVO_PAGES_001`: invalid resolved-page cache limit.
- `TAVO_PAGES_002`: missing client root element.
- `TAVO_PAGES_003` / 004 / 005: page discovery or server bootstrap failure.
- `TAVO_SSR_001`: invalid canonical SSR origin.
- `TAVO_CONFIG_001` / 002: server-only or likely secret code reached a client boundary.
- `TAVO_PLUGIN_001`: plugin descriptor targets an unsupported contract.
- `TAVO_HYDRATION_001`: strict hydration found a server/client mismatch.
## Instrumentation event contract
Build a private observer with [](/docs/core/api/dev#api-tavojs-core-dev--createinstrumentation "View createInstrumentation in the Core API reference"), or adapt an OpenTelemetry tracer with [](/docs/core/api/dev#api-tavojs-core-dev--createopentelemetryinstrumentation "View createOpenTelemetryInstrumentation in the Core API reference"). Both produce a [](/docs/core/api/dev#api-tavojs-core-dev--tavoinstrumentation "View TavoInstrumentation in the Core API reference")value for the SSR configuration.
API / option
Type
Default
Behavior
`name`
`"route.resolve" | "route.middleware" | "route.loader" | "route.action" | "route.cache"`
—
Identifies the framework operation.
`phase`
`"start" | "end" | "error" | "abort" | "hit" | "miss" | "invalidate"`
—
Identifies lifecycle state and cache outcomes.
`timing/context`
`timestamp, durationMs, requestId, route, layer`
operation-specific
Correlates work without including request content.
`result/cache`
`status, count, cacheTags, error`
operation-specific
Carries bounded result metadata. Treat custom error objects as potentially sensitive.
- Observer exceptions are isolated and never interrupt framework work.
- The `OpenTelemetry` adapter pairs start and terminal events into spans.
- `recordErrors` defaults to false; enable it only after application-level redaction is configured.
- Request bodies, headers, cookies, tokens, loader data, and store state are not emitted by the framework.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Scheduling and instrumentation
> Prioritize browser updates and observe server route lifecycles through the public experimental runtime tooling boundary.
Canonical page: https://tavojs.dev/docs/core/scheduling-and-instrumentation
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A browser update-priority need or a server observability integration.
## Outcomes
- Choose the correct update priority.
- Defer non-urgent rendering with startTransition.
- Use flushSync only for an immediate DOM ordering requirement.
- Attach isolated instrumentation to the pages runtime.
- Adapt route events to an OpenTelemetry-compatible tracer.
- Keep error and request data out of telemetry by default.
## Understand the priority queue
Scheduling APIs are public experimental exports from `@tavojs/core/dev`. They change when a connected component update renders; they do not make synchronous business logic asynchronous and they do not prioritize network requests.
API / contract
Type / boundary
Default / result
Behavior
`immediate`
[`UpdatePriority`](/docs/core/api/dev#api-tavojs-core-dev--updatepriority)
explicit
Queues work that `flushSync` drains before returning from its callback.
`user-blocking`
[`UpdatePriority`](/docs/core/api/dev#api-tavojs-core-dev--updatepriority)
explicit
Higher-priority interactive work flushed with the normal microtask queue.
`normal`
[`UpdatePriority`](/docs/core/api/dev#api-tavojs-core-dev--updatepriority)
current default
Connected component updates flush in a microtask.
`background`
[`UpdatePriority`](/docs/core/api/dev#api-tavojs-core-dev--updatepriority)
startTransition
Non-urgent updates flush from a timer turn.
`idle`
[`UpdatePriority`](/docs/core/api/dev#api-tavojs-core-dev--updatepriority)
explicit
Uses `requestIdleCallback` when available and a short timer fallback otherwise.
- A component queued more than once keeps its highest requested priority and renders once for the accumulated state.
- `runWithUpdatePriority` restores the previous priority in a finally block, including after a callback throws.
- `getCurrentUpdatePriority` reports the active callback priority; outside an override it reports normal.
**Reference src/scheduling.ts**
```ts
import {
getCurrentUpdatePriority,
runWithUpdatePriority,
} from "@tavojs/core/dev";
runWithUpdatePriority("user-blocking", () => {
console.debug(getCurrentUpdatePriority()); // "user-blocking"
updateKeyboardSelection();
});
```
## Defer a non-urgent result update
- Keep the controlled input update urgent and defer only the replaceable result rendering.
- Cancellation and stale-result handling still belong to the controller or resource that owns asynchronous work.
- Do not use a transition for accessibility state that must be announced immediately.
**Reference src/components/ProjectSearch/index.tsx**
```tsx
import { TavoController, createTavo } from "@tavojs/core";
import { startTransition } from "@tavojs/core/dev";
import { Input, Stack, Text } from "@tavojs/ui";
type SearchState = {
query: string;
visibleProjects: string[];
};
class ProjectSearchController extends TavoController {
updateQuery(query: string) {
this.model.patch({ query });
startTransition(() => {
this.model.patch({
visibleProjects: filterProjects(query)
});
});
}
}
export const ProjectSearch = createTavo<{}, SearchState>({
model: function createSearchState() {
return {
query: "",
visibleProjects: []
};
},
controller: ProjectSearchController,
view: function ProjectSearchView({ state, controller }) {
return (
{
controller?.updateQuery(event.currentTarget.value);
}}
/>
{state.visibleProjects.length} projects
);
}
});
```
## Flush only when browser ordering requires it
`flushSync` runs its callback at immediate priority and drains immediate connected component work before returning. It is appropriate when the next statement must observe the updated DOM, such as measurement or focus handoff.
**Reference src/components/Disclosure/controller.ts**
```ts
import { flushSync } from "@tavojs/core/dev";
export function openAndFocus(
open: () => void,
focusPanel: () => void
): void {
flushSync(() => {
open();
});
focusPanel();
}
```
**Synchronous rendering is a narrow escape hatch**
Do not wrap ordinary event handlers in `flushSync`. It reduces batching opportunities and can make interaction slower. Prefer the normal queue unless the next browser operation truly depends on the committed DOM.
## Verify the user-visible effect
- Use runtime devtools to confirm that pending updates return to zero after the interaction.
- Test that urgent input remains responsive while transition work is pending.
- Test focus or measurement code in a real DOM environment; a server string render cannot verify browser ordering.
- Avoid tests that depend on exact timer milliseconds. Assert final state and ordering instead.
## Read the lifecycle event contract
API / contract
Type / boundary
Default / result
Behavior
`name`
`route.resolve | middleware | loader | action | cache`
operation-specific
Identifies the framework operation being observed.
`phase`
`start | end | error | abort | hit | miss | invalidate`
operation-specific
Identifies lifecycle progress, cancellation, or a cache outcome.
`timestamp / durationMs`
`number`
Date.now / terminal only
Provides wall-clock correlation and elapsed time.
`requestId / route / layer`
`string`
when available
Correlates related work without including a URL query, headers, or data payload.
`status / count / cacheTags`
`bounded result metadata`
when available
Reports HTTP and cache outcomes.
`error`
`unknown`
error phase only
Potentially sensitive application object; adapters do not record it unless explicitly enabled.
- Framework events do not include request bodies, headers, cookies, tokens, loader results, or store state.
- Listener failures are caught and never alter route behavior.
- A custom `TavoInstrumentation` implementation receives the same isolation guarantee as `createInstrumentation`.
## Create an isolated custom observer
- Keep the observer synchronous and inexpensive. Buffer or enqueue slow exporter work outside the request path.
- Use route patterns rather than raw pathnames as metric labels to avoid unbounded cardinality.
- Do not throw from a listener to signal exporter failure; monitor the exporter separately.
**Reference instrumentation.ts**
```ts
import "@tavojs/core/server-only";
import {
createInstrumentation,
type TavoInstrumentationEvent
} from "@tavojs/core/dev";
function recordRouteMetric(event: TavoInstrumentationEvent): void {
if (event.phase !== "end" || event.durationMs === undefined) {
return;
}
metrics.histogram("tavo.route.duration", event.durationMs, {
operation: event.name,
route: event.route ?? "unknown"
});
}
export const instrumentation = createInstrumentation(
function observeTavoEvent(event) {
recordRouteMetric(event);
}
);
```
**Reference tavo.config.ts**
```ts
import { defineConfig } from "@tavojs/core/config";
import { instrumentation } from "./instrumentation";
export default defineConfig({
ssr: {
instrumentation
}
});
```
## Adapt events to an OpenTelemetry tracer
[](/docs/core/api/dev#api-tavojs-core-dev--createopentelemetryinstrumentation "View createOpenTelemetryInstrumentation in the Core API reference")accepts the stable subset shared by OpenTelemetry tracer implementations. Supply an [](/docs/core/api/dev#api-tavojs-core-dev--opentelemetrytracerlike "View OpenTelemetryTracerLike in the Core API reference")adapter; its spans implement [](/docs/core/api/dev#api-tavojs-core-dev--opentelemetryspanlike "View OpenTelemetrySpanLike in the Core API reference"). Start events create spans. End, error, abort, hit, miss, and invalidate events finish the matching span or create a bounded terminal span when no start is pending.
- Span names use `tavo.route.resolve`, `tavo.route.middleware`, `tavo.route.loader`, `tavo.route.action`, or `tavo.route.cache`.
- Correlation uses request ID, event name, route, and layer. Concurrent matching starts are completed in order.
- `recordErrors` defaults to false. Enable it only after application-level exception redaction is configured.
- Error and abort phases set an error status; normal and cache terminal phases set success.
**Reference src/server/telemetry-contract.ts**
```ts
export type OpenTelemetrySpanLike = {
setAttribute?(
name: string,
value: string | number | boolean
): unknown;
recordException?(error: unknown): unknown;
setStatus?(status: {
code: number;
message?: string;
}): unknown;
end?(endTime?: number): unknown;
};
export type OpenTelemetryTracerLike = {
startSpan(
name: string,
options?: {
attributes?: Record;
startTime?: number;
}
): OpenTelemetrySpanLike;
};
```
**Reference src/server/instrumentation.ts**
```ts
import "@tavojs/core/server-only";
import {
createOpenTelemetryInstrumentation
} from "@tavojs/core/dev";
import { tracer } from "./telemetry";
export const instrumentation = createOpenTelemetryInstrumentation(
tracer,
{
recordErrors: false
}
);
```
**Observability must not become a data export**
Route names and cache tags are operational metadata, but they can still reveal application structure. Review exporter access, retention, and label cardinality as part of production security.
## Verify lifecycle and failure isolation
- Assert start and terminal events share request, route, and layer identity.
- Abort a navigation or disconnect a request and verify an abort event rather than a false success.
- Make a test listener throw and verify the route still completes.
- Keep telemetry setup behind a server-only boundary and never import a private exporter into browser code.
**Run Terminal**
```bash
npm run typecheck
npx tavo build
npx tavo preview --ssr
# Exercise one loader, one action, and one cache hit.
# Confirm the exporter receives terminal events and the responses are unchanged.
```
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Advanced development tooling
> Build custom route inspectors, runtime panels, overlays, and SSR development hosts on the public experimental development boundary.
Canonical page: https://tavojs.dev/docs/core/development-tooling
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A custom development, test, editor, or inspection integration.
- Ordinary application development should use the project-local Tavo.js CLI first.
## Outcomes
- Inspect route modules without rendering them.
- Subscribe to privacy-safe runtime state and clean up correctly.
- Start and close a custom SSR Vite development host.
## Use the public experimental boundary
`@tavojs/core/dev` is a public experimental entry point. Its exports are suitable for custom development hosts and tools, but can evolve faster than stable application entry points. Import only this package boundary; files under Core src/runtime, src/ssr, src/framework, and src/auto-pages are not public imports.
API / contract
Type / boundary
Default / result
Behavior
`route inspection`
`build / server`
read-only
Discover modules and build manifests or diagnostics without creating a route runtime.
`runtime snapshots`
`browser`
privacy-safe
Inspect route lifecycle and DOM counts without loader data, headers, cookies, tokens, or store values.
`diagnostics / overlay`
`browser development`
opt-in
Collect traces and hydration mismatches or display a dependency-free error overlay.
`SSR Vite host`
`Node development`
127.0.0.1:4174
Starts middleware-mode Vite with `Tavo.js` routes, actions, plugins, images, and development cache behavior.
## Inspect route modules without rendering
- The CLI is the normal application inspection surface and owns route discovery.
- Experimental hosts that already own an explicit module map can use `createPagesManifest`, `createPagesManifestDetailed`, and `inspectPages` from `@tavojs/core/dev`.
- `createPagesManifest` returns sorted routes. `createPagesManifestDetailed` also returns 404, global error, and diagnostics metadata.
**Run Terminal**
```bash
npx tavo routes
npx tavo inspect route /dashboard --json
```
## Subscribe to runtime state and dispose
- `subscribeTavoRuntime` emits immediately unless immediate: false is supplied.
- The subscription follows pathname and route-status changes and returns one unsubscribe function.
- The panel returns element, refresh, and dispose. dispose removes its subscription, event listener, and DOM element.
- Snapshots omit application data but expose route structure and operational counts; keep production installation opt-in.
**Reference src/dev/runtime-inspection.ts**
```ts
import {
inspectTavoRuntime,
installTavoDevtoolsPanel,
subscribeTavoRuntime
} from "@tavojs/core/dev";
export function installRuntimeInspection(): () => void {
console.debug(inspectTavoRuntime());
const stop = subscribeTavoRuntime(
function printRuntimeSnapshot(snapshot) {
console.debug(snapshot.route, snapshot.status, snapshot.dom);
},
{ immediate: false }
);
const panel = installTavoDevtoolsPanel({
initiallyOpen: false
});
return function disposeRuntimeInspection() {
stop();
panel.dispose();
};
}
```
## Configure diagnostics and the development overlay
API / contract
Type / boundary
Default / result
Behavior
[`configureDevDiagnostics`](/docs/core/api/dev#api-tavojs-core-dev--configuredevdiagnostics)
`browser diagnostics`
disabled
Configures traces, mismatch callbacks, error handling, development reporting, and strict hydration.
[`installDevOverlay`](/docs/core/api/dev#api-tavojs-core-dev--installdevoverlay)
`browser development`
traces: false
Installs the error overlay; optional traces add development lifecycle context.
**Reference src/dev.ts**
```ts
import {
configureDevDiagnostics,
installDevOverlay,
} from "@tavojs/core/dev";
configureDevDiagnostics({
enabled: true,
devMode: true,
strictHydration: false,
});
installDevOverlay({ traces: true });
```
**Strict hydration belongs in verification**
`strictHydration` throws `TAVO_HYDRATION_001` at the first mismatch. Use it in browser tests or a controlled development mode, fix the first divergence, and do not enable it as an unreviewed production failure policy.
## Start and close a custom SSR development host
- The host reads root `tavo.config.ts` in the selected mode and uses its pages, CSS, plugins, and nested SSR options.
- Set host deliberately. Binding 0.0.0.0 exposes the development server to the local network.
- `TAVO_MONITOR_TOKEN` protects the development monitor endpoint with an exact Bearer header.
- Always await `server.close` in tests and editor integrations so Vite watchers and the HTTP listener are released.
**Reference scripts/dev-ssr.ts**
```ts
import {
startViteAutoPagesDevServer
} from "@tavojs/core/dev";
async function main(): Promise {
const server = await startViteAutoPagesDevServer({
root: process.cwd(),
mode: "development",
host: "127.0.0.1",
port: 4174
});
console.log(server.url);
process.once("SIGTERM", function closeServer() {
void server.close();
});
}
await main();
```
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Runtime devtools reference
> Inspect privacy-safe route, cache, module, component, effect, and scheduler state in the browser.
Canonical page: https://tavojs.dev/docs/core/runtime-devtools
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: browser
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A browser-rendered Tavo.js application in development.
## Outcomes
- Inspect privacy-safe runtime snapshots and manage the optional devtools panel.
## Snapshot and subscription APIs
**Reference Reference snippet**
```ts
import {
inspectTavoRuntime,
installTavoDevtoolsPanel,
subscribeTavoRuntime,
} from "@tavojs/core/dev";
console.debug(inspectTavoRuntime());
const stop = subscribeTavoRuntime((snapshot) => {
console.debug(snapshot.route, snapshot.status);
});
const panel = installTavoDevtoolsPanel();
export function disposeDevtools() {
stop();
panel.dispose();
}
```
API / option
Type
Default
Behavior
[`inspectTavoRuntime`](/docs/core/api/dev#api-tavojs-core-dev--inspecttavoruntime)`(``)`
[`TavoDevtoolsSnapshot`](/docs/core/api/dev#api-tavojs-core-dev--tavodevtoolssnapshot)
current state
Returns pathname, route, status, params, route runtime inspection, and DOM counts.
[`subscribeTavoRuntime`](/docs/core/api/dev#api-tavojs-core-dev--subscribetavoruntime)`(``listener``,` `options``)`
`unsubscribe function`
immediate: true
Publishes on navigation and route-status changes; pass immediate: false to skip the first callback.
[`installTavoDevtoolsPanel`](/docs/core/api/dev#api-tavojs-core-dev--installtavodevtoolspanel)`(``options``)`
[`TavoDevtoolsPanel`](/docs/core/api/dev#api-tavojs-core-dev--tavodevtoolspanel)
document.body, closed
Installs a dependency-free browser panel. It throws when no DOM document exists.
`panel.refresh()`
`void`
—
Refreshes the JSON snapshot manually.
`panel.dispose()`
`void`
—
Unsubscribes listeners, removes events, and removes the panel element.
**Privacy-safe does not mean public**
Framework snapshots omit loader data, store values, headers, cookies, and tokens. Route names and operational counts can still reveal application structure, so keep production devtools opt-in.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Framework CLI automation protocol
> Consume versioned CLI JSON envelopes, bounded context, transactional change plans, receipts, and restricted verification safely.
Canonical page: https://tavojs.dev/docs/core/cli-automation-protocol
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A project-local tavo CLI matching the application lockfile.
- An editor, CI job, or automation client that checks process exit status.
## Outcomes
- Parse the stable v1 machine envelope.
- Apply hash-guarded changes transactionally.
- Verify changes without executing project scripts when required.
## Parse the version 1 envelope
JSON-capable CLI commands return a versioned machine envelope. Parseable JSON does not imply success: check the process exit status, ok, and error-level diagnostics.
API / contract
Type / boundary
Default / result
Behavior
`schemaVersion`
`1`
required
Machine protocol version.
`command`
`string`
required
Canonical producing command identifier.
`ok`
`boolean`
derived
False when the command or returned diagnostics contain an error.
`project.fingerprint`
`16 lowercase hex characters`
required
Bounded fingerprint of command-relevant project data, not a file content hash.
`data`
`command-specific`
required
Payload described by the command or companion schema.
`diagnostics`
`Diagnostic v1[]`
\[\]
Contains code, error/warning level, message, and optional location, confidence, fix, docs, or commands.
`nextActions`
`{ command, reason }[]`
\[\]
Suggested next commands; never implicit authorization.
`metrics`
`durationMs / bytes / estimatedTokens`
required
Bounded transport and execution metadata.
- The installed package ships schemas under `node_modules`/`@tavojs/cli/schemas`.
- Generator input uses `node_modules`/`@tavojs/cli/generator-spec.schema.json`.
- agent-context data declares `protocolVersion`: 1 and `protocolStability`: stable.
## Request the smallest useful context
- Summary context contains conventions, focused source metadata, relevant API cards, recipes, commands, and bounded project information.
- Request full detail only when a complete route or inventory graph is necessary.
- Focused inspection includes a SHA-256 content hash where available; use that hash as a write precondition.
**Run Terminal**
```bash
npx tavo agent-context --json --task modify-route --target /account --detail summary
npx tavo inspect route /account --json
```
## Use explicit transactional operations
A change plan contains between one and 100 versioned operations. Existing-file mutations require a 64-character SHA-256 precondition; low-risk diagnostic fixes require either `expectedSha256` or `expectedMissing`.
API / contract
Type / boundary
Default / result
Behavior
`generate`
`generator spec`
planned write
Runs one validated `Tavo.js` generation specification.
`create-file`
`path + content`
new file
Rejects an unsafe, escaping, or existing target.
`replace-range`
`hash + 1-based range`
bounded edit
Rejects stale content before replacing the exact source range.
`delete-file`
`path + hash`
transactional delete
Rejects a stale or escaping target.
`apply-fix`
`diagnostic code + precondition`
low-risk fix only
Applies a CLI-owned safe fix identified by its diagnostic.
- Each text payload is limited to 1 `MiB`.
- Traversal and symlink escapes are rejected.
- If a later operation fails, earlier writes are rolled back.
**Reference change-plan.json**
```json
{
"schemaVersion": 1,
"operations": [
{
"id": "update-account-title",
"kind": "replace-range",
"file": "src/pages/account.tsx",
"expectedSha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"range": {
"start": { "line": 12, "column": 3 },
"end": { "line": 12, "column": 24 }
},
"text": "Account settings"
}
]
}
```
## Retain and verify the receipt
API / contract
Type / boundary
Default / result
Behavior
`dryRun`
`boolean`
required
Distinguishes a planned receipt from an applied change.
`transaction`
`planned | committed | rolled-back | rejected`
required
Reports the final transaction state.
`fileHashes`
`path → SHA-256 | null`
required
Records post-operation file state for focused verification.
`operations`
`operation result[]`
required
Reports each planned or attempted operation.
`verificationCommand`
`string`
required
Suggested focused verify command for the receipt.
- change prints a protocol envelope to stdout; it does not create a receipt file automatically.
- Save stdout only after checking that the command completed successfully.
- verify expands affected Framework surfaces from the receipt and checks current hashes.
- `--no-project-scripts` prevents verify from running `package.json` scripts. Confirm `data.projectScripts` is false.
**Run Terminal**
```bash
npx tavo change --from-json change-plan.json --dry-run
npx tavo change --from-json change-plan.json > change-receipt.json
npx tavo verify --receipt change-receipt.json --smoke --no-project-scripts --json
```
## Use stdin and specs deliberately
- Generator specs support page, component, store, layout, 404, error, action, and feature records.
- Page specs use `typedRoute`: true only when `defineRoutePage` generation is desired.
- Validation and dry-run prove structural validity and write scope; they do not approve the product change.
- Use `--force` only after inspecting an intended replacement.
**Run Terminal**
```bash
npx tavo generate --validate-spec tavo.generated.json
npx tavo generate --from-json tavo.generated.json --dry-run
npx tavo generate --from-stdin --dry-run < tavo.generated.json
npx tavo change --from-stdin --dry-run < change-plan.json
```
## Verify semantic CLI behavior
- `npx` `tavo` inspect plugins `--json` performs plugin preflight and reports owners, permissions, reasons, exposure, and diagnostics.
- Build flags override configured `JavaScript` budgets and `prerenderStyles` for that invocation.
- Plain `tavo` preview delegates to Vite preview. `tavo` preview `--ssr` rebuilds when production output is missing or stale.
- Use the project-local CLI resolved from the lockfile. Use `npx` `@tavojs/cli` only for initial application creation.
- Use `--help` on the installed version as the exact command/flag inventory; use the authored references for side effects, security, precedence, and failure behavior.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Deployment
> Build once and deploy either the provider-neutral static output or generated Node server.
Canonical page: https://tavojs.dev/docs/core/deployment
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: guide
Runtime: server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js project with dependencies installed.
## Outcomes
- Build once and deploy either the provider-neutral static output or generated Node server.
## Know what the build produces
`tavo` build emits two provider-neutral deployment outputs. Treat .`tavo`/build as generated output and rebuild it after every application change.
**Reference Generated output — after a production build**
```text
.tavo/build/client/ static assets and prerendered HTML
.tavo/build/server/start.mjs generated Node production server
```
## Choose static hosting or Node
Static hosting can serve CSR and prerendered output but cannot run server loaders, route actions, or sessions. Run the generated Node server when the application needs request-time behavior.
**Run Terminal — run from the project root**
```bash
npx tavo build
PORT=4174 node .tavo/build/server/start.mjs
```
## Verify the deployed boundary
Business logic stays in route modules and server-only application services. Core 1.0 publishes static and Node deployment outputs.
- Rebuild after every source change.
- Publish .`tavo`/build/client to a static host or run `.tavo/build/server/start.mjs` with Node.
- Configure trusted hosts, canonical origin, CSP, secrets, and monitor authentication.
- Exercise a real SSR request, a client navigation, an action, and an error response.
- Inspect generated route sizes and enforce budgets in CI.
## Build artifact contract
**Reference Reference snippet**
```text
.tavo/build/client/
static assets and prerendered HTML
.tavo/build/server/start.mjs
generated Node production server
```
tavo build creates both provider-neutral outputs, discovers routes, generates route types, and attempts eligible prerenders. Invalid bundles or JavaScript budget violations fail the command.
**SSR preview rebuilds stale output**
tavo preview --ssr checks whether the production build is missing or older than application sources and runs tavo build first when necessary. Plain tavo preview delegates to Vite preview.
## Static and Node production output
API / option
Type
Default
Behavior
`.tavo/build/client`
`directory`
always generated
Static assets and prerendered HTML suitable for any static host.
`.tavo/build/server/start.mjs`
`Node entry`
always generated
Generated production server for SSR, loaders, actions, sessions, plugins, and monitoring.
## Production handler failure behavior
- GET and HEAD render pages. Other methods dispatch a matching route action.
- A non-page method without a matching action returns 405 with Allow: GET, HEAD.
- The Node handler rejects bodies larger than `maxRequestBodyBytes` with 413; the default is 10 `MiB`.
- Uncaught route, plugin, and handler failures return a hardened generic 500 response rather than exposing an exception.
- `canonicalOrigin` must be a credential-free HTTP(S) origin with no path, query, or hash; invalid input throws `TAVO_SSR_001`.
- Page and action responses receive baseline security headers. Add deployment-specific CSP and proxy policy at the platform boundary.
- Node client disconnects abort request-owned work; streaming responses also cancel their active reader.
## Monitor CLI defaults
API / option
Type
Default
Behavior
`--url`
`URL`
http://127.0.0.1:4174
The CLI appends /\_tavo/monitor unless it is already present.
`--token`
`string`
TAVO\_MONITOR\_TOKEN
Sent as a Bearer token. Never place monitor credentials in a query string.
`--once`
`boolean`
false
Without this flag, monitor refreshes continuously.
`--interval`
`milliseconds`
1000
Watch interval, clamped to a minimum of 250 ms.
`--json`
`boolean`
false
Prints the complete payload instead of the human table.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Runtime configuration reference
> Review page runtime, document, SSR handler, cache, and image options with their production defaults.
Canonical page: https://tavojs.dev/docs/core/runtime-configuration
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application that needs a runtime option or default.
## Outcomes
- Find page runtime, document, SSR handler, cache, and image option defaults.
## Configure shared route behavior through defineConfig
API / option
Type
Default
Behavior
`ssr.getPageProps`
`() => record`
unset
Adds application props to every page component.
`ssr``.`[`notFound`](/docs/core/api/router#api-tavojs-core-router--notfound)
[`Component`](/docs/core/api/components-and-dom#api-tavojs-core--component)
src/pages/404
Overrides the discovered not-found component.
`ssr.csrFallback`
[`Child`](/docs/core/api/components-and-dom#api-tavojs-core--child) `|` `function`
empty route node
Server shell for CSR routes.
`ssr.csrActions`
`CsrActionsOptions`
disabled
Routes browser form submissions to a configured action endpoint.
`ssr.middleware`
[`PageMiddleware`](/docs/core/api/router#api-tavojs-core-router--pagemiddleware)`[``]`
\[\]
Runs before route and layout middleware.
`ssr.allowExternalRedirects`
`boolean`
false
Permits normalized redirects to another origin.
`ssr.trustedHosts`
`string[]`
local host policy
Allows inbound hosts for action-origin validation.
`ssr.i18n`
[`I18nService`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--i18nservice)
registered default
Resolves locale-aware paths, messages, and document direction.
`plugins`
[`TavoPluginInput`](/docs/core/api/plugin#api-tavojs-core-plugin--tavoplugininput)
\[\]
Accepts the public plugin array or { use, overrides } form and adds manifest-declared pages, middleware, capabilities, stores, head entries, endpoints, and build contributions.
`ssr.maxResolvedCacheEntries`
`number`
1024
Bounds process-local resolved static route data. Zero disables reuse; invalid values throw `TAVO_PAGES_001`.
`ssr.instrumentation`
[`TavoInstrumentation`](/docs/core/api/dev#api-tavojs-core-dev--tavoinstrumentation)
unset
Receives isolated route lifecycle events.
## Document rendering options
API / option
Type
Default
Behavior
`lang`
`string`
"en"
HTML language unless the resolved i18n locale overrides it.
`title`
`string`
""
Escaped base document title; resolved route and component metadata can replace it.
`unsafeHeadHtml`
`string`
""
Trusted raw HTML inserted into the document head. Prefer escaped TSX metadata and use this explicit unsafe boundary only for reviewed markup.
`htmlAttributes / bodyAttributes / appAttributes`
`record`
{}
Safe string, number, or boolean attributes.
`doctype`
`string`
""
Document prefix.
`appContainerId`
`string`
"app"
ID of the rendered application container.
`initialState`
`unknown`
omitted
Serialized as escaped JSON when defined.
`stateScriptId`
`string`
"\_\_TAVO\_STATE\_\_"
ID of the JSON hydration script.
`nonce`
`string`
unset
Applied to state, style, and deferred patch scripts where supported.
`beforeRender`
`() => void`
unset
Re-establishes request context before render passes and stream chunks.
`styleRegistry`
[`StyleRegistry`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--styleregistry)
new registry
Collects SSR component styles with deduplication.
## SSR handler options
API / option
Type
Default
Behavior
`modules`
`PageModules`
required
Route module map used to create the pages runtime.
`canonicalOrigin`
`string`
request host
Node-only public HTTP(S) origin behind TLS termination. Invalid origins throw `TAVO_SSR_001`.
`document`
[`RenderDocumentOptions`](/docs/core/api/server#api-tavojs-core-server--renderdocumentoptions)
{}
Base HTML shell and CSP settings.
`stream`
`boolean`
false
Returns streamed HTML and enables progressive [`Deferred`](/docs/core/api/data-actions-and-async#api-tavojs-core--deferred "View Deferred in the Core API reference") patches.
`images`
[`ImageOptimizerOptions`](/docs/core/api/server#api-tavojs-core-server--imageoptimizeroptions)
optimizer defaults
Configures /\_tavo/image processing.
`staticCache`
[`SsrStaticCache`](/docs/core/api/server#api-tavojs-core-server--ssrstaticcache)
memory, 1024 entries
Rendered-response cache adapter.
`maxRequestBodyBytes`
`number`
10485760
Node-only action body limit; excess input receives 413.
## Image optimizer defaults
API / option
Type
Default
Behavior
`enabled`
`boolean`
true
Enables the optimizer endpoint when image options are used.
`allowRemote`
`boolean`
false
Remote sources remain blocked until explicitly enabled and allowlisted.
`publicDir`
`string`
"public"
[`Root`](/docs/core/api/components-and-dom#api-tavojs-core--root "View Root in the Core API reference") for absolute local image paths.
`quality`
`number`
75
Default output quality.
`cacheMaxAge`
`seconds`
31536000
Successful response cache lifetime.
`defaultFormat`
[`ImageFormat`](/docs/core/api/localization-seo-and-assets#api-tavojs-core--imageformat)
"webp"
Output format when negotiation does not select another configured format.
`sizes`
`number[]`
320, 640, 960, 1280, 1600
Allowed responsive widths.
`timeoutMs`
`number`
5000
Remote fetch timeout.
`maxBytes`
`number`
10485760
Maximum source image size.
`memoryCacheMaxEntries`
`number`
128
Process-local optimized image entries.
`maxConcurrentTransforms`
`number`
4
Active transformations, clamped to at least one.
`maxPendingTransforms`
`number`
64
Queued transformations before the endpoint returns 503.
`allowInsecureRemote`
`boolean`
false
HTTP and private-network protections remain enabled by default.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Runtime diagnostics reference
> Configure render traces, hydration mismatch reports, strict CI failures, and DOM runtime tuning.
Canonical page: https://tavojs.dev/docs/core/runtime-diagnostics
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A Tavo.js application with a reproducible runtime problem.
## Outcomes
- Configure runtime traces and hydration mismatch reporting.
- Interpret diagnostic hooks without leaking application data.
## Diagnostic options
Apply these process-wide development settings with [](/docs/core/api/dev#api-tavojs-core-dev--configuredevdiagnostics "View configureDevDiagnostics in the Core API reference").
**Reference Reference snippet**
```ts
import { configureDevDiagnostics } from "@tavojs/core/dev";
configureDevDiagnostics({
enabled: true,
devMode: true,
strictHydration: true,
onHydrationMismatch(event) {
console.error(event.kind, event.path, event.recovery);
},
});
```
API / option
Type
Default
Behavior
`enabled`
`boolean`
false
Enables trace delivery when an `onTrace` callback exists.
`devMode`
`boolean`
false
Collects mismatch details and enables console/overlay development reporting.
`onTrace`
`callback | null`
null
Receives mount, patch, and hydrate events.
`onHydrationMismatch`
`callback | null`
null
Receives expected/found values, DOM path, phase, kind, and recovery mode.
`onError`
`callback | null`
null
Receives runtime errors instead of the console fallback.
`strictHydration`
`boolean`
false
Throws `TAVO_HYDRATION_001` on the first mismatch, suitable for CI/browser tests.
Recovery is reported as text replacement, subtree replacement, or extra-node cleanup. Fix the first mismatch rather than suppressing later symptoms.
## Use the DOM runtime defaults
Normal applications use the renderer defaults. DOM reconciliation tuning is not part of the public application API; use stable keys and fix hydration mismatches reported by the diagnostics hooks.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Framework diagnostics reference
> Map every stable framework diagnostic code to its boundary, structured error contract, and first remediation step.
Canonical page: https://tavojs.dev/docs/core/diagnostics
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A framework error, CLI diagnostic, plugin graph failure, or hydration mismatch to investigate.
## Outcomes
- Recognize every stable Core diagnostic family.
- Preserve structured codes and remediation hints in tooling.
- Choose runtime diagnostics or CLI verification for the affected boundary.
## Preserve the structured TavoError
`TavoError` carries a stable code, message, optional structured details, remediation hint, and cause. Check `isTavoError` before reading those fields. `formatTavoError` preserves the code and appends the hint for human output.
- Branch on code, not on message text.
- Treat details and cause as potentially sensitive before logging them.
- Do not convert an error code into a successful process exit in build or verification tooling.
**Reference src/server/startup.ts**
```ts
import {
formatTavoError,
isTavoError
} from "@tavojs/core";
export async function startApplication(): Promise {
try {
await initializeApplication();
} catch (error) {
if (isTavoError(error)) {
logger.error({
code: error.code,
details: error.details,
message: formatTavoError(error)
});
return;
}
throw error;
}
}
```
## Pages, SSR, and configuration codes
API / contract
Type / boundary
Default / result
Behavior
`TAVO_PAGES_001`
`runtime configuration`
error
The resolved-page cache limit is invalid. Supply a finite non-negative `maxResolvedCacheEntries` value.
`TAVO_PAGES_002`
`browser boot`
error
The client root element is missing. Align `bootTavo` `rootId` with the HTML shell.
`TAVO_PAGES_003`
`page discovery`
error
A dynamic discovery pattern is unsupported. Use the standard literal glob or explicit modules.
`TAVO_PAGES_004`
`page discovery`
error
The bundler page-discovery API is unavailable. Use `Tavo.js`'s Vite wrapper or explicit modules.
`TAVO_PAGES_005`
`server bootstrap`
error
The server bootstrap is missing page modules. Pass the discovered modules map.
`TAVO_PAGES_006`
`route manifest`
error
A page declares conflicting static generation options. Choose named prerender or helper static, not both.
`TAVO_SSR_001`
`Node handler`
error
`canonicalOrigin` is not a credential-free HTTP(S) origin without a path, query, or hash.
`TAVO_CONFIG_001`
`client build`
error
A server-only module reached the browser graph. Move the import behind a server loader, action, middleware, or server plugin phase.
`TAVO_CONFIG_002`
`client build`
error
A likely secret environment value is referenced from browser code. Keep it server-only and expose only deliberate VITE\_ values.
`TAVO_HYDRATION_001`
`strict hydration`
error
Strict hydration found a server/client mismatch. Fix the first reported DOM path and phase.
## Plugin API v1 codes
API / contract
Type / boundary
Default / result
Behavior
`TAVO_PLUGIN_001`
`compatibility`
error
The descriptor is missing Plugin API version 1 or targets an unsupported version.
`TAVO_PLUGIN_002`
`identity / manifest`
error
Plugin identity, configuration input, manifest authority, or a hydrated store declaration is invalid.
`TAVO_PLUGIN_003`
`ownership`
error
An installation or contribution duplicates ownership without an exact valid override.
`TAVO_PLUGIN_004`
`dependency / capability`
error
A required plugin, version, capability, request resource, or declared owner is unavailable.
`TAVO_PLUGIN_005`
`dependency / ordering`
error
A plugin dependency, middleware order, or capability resolution graph contains a cycle.
`TAVO_PLUGIN_006`
`authority / permission`
error
A plugin requested a reserved resource, undeclared exposure, invalid override, or missing permission.
`TAVO_PLUGIN_007`
`phase implementation`
error
A loaded phase does not implement exactly the contributions declared by its manifest.
`TAVO_PLUGIN_008`
`initialize / build`
error
A phase loader, setup hook, capability/store factory, or build contribution failed.
`TAVO_PLUGIN_009`
`request / dispose`
error
Plugin middleware, an endpoint, a request capability, or request/runtime disposal failed.
- Plugin graph diagnostics include severity, phase, message, and optional resource, owners, and hint.
- Use `tavo` inspect plugins or `tavo` inspect plugins `--json` for the supported project workflow.
- Experimental tooling can import `inspectPluginGraph` from `@tavojs/core/dev`; compilation, request dispatch, and runtime disposal remain framework host responsibilities.
## Capture browser runtime context
**Reference src/dev/diagnostics.ts**
```ts
import {
configureDevDiagnostics
} from "@tavojs/core/dev";
export function enableStrictDiagnostics(): () => void {
configureDevDiagnostics({
enabled: true,
devMode: true,
strictHydration: true,
onTrace(event) {
console.debug(event.phase, event.path);
},
onHydrationMismatch(event) {
console.error(event.path, event.kind, event.recovery);
},
onError(error) {
console.error(error);
}
});
return function disableStrictDiagnostics() {
configureDevDiagnostics({
enabled: false,
devMode: false,
strictHydration: false,
onTrace: null,
onHydrationMismatch: null,
onError: null
});
};
}
```
**Fix the first mismatch**
Hydration recovery may replace text, replace a subtree, or remove extra nodes. Later mismatch reports can be consequences of the first divergence, so start with the earliest DOM path and phase.
## Choose the matching verifier
- doctor reports project-shape issues without performing a production build.
- check adds the project's typecheck script when available.
- inspect plugins validates ownership and authority before phases are executed.
- verify `--smoke` adds lightweight route checks; build remains the production compiler and prerender authority.
- A parseable JSON envelope can still represent failure. Check both ok and the process exit status.
**Run Terminal**
```bash
npx tavo doctor --json
npx tavo check --json
npx tavo inspect plugins --json
npx tavo verify --smoke --json
npx tavo build
```
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# API stability
> Distinguish stable application entry points from experimental low-level runtime and tooling contracts.
Canonical page: https://tavojs.dev/docs/core/api-stability
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- A public Tavo.js package entry point you plan to depend on.
## Outcomes
- Distinguish stable application APIs from experimental low-level contracts.
## Read the stability contract
Stable means changes follow semantic versioning from the 1.0 baseline onward. Experimental APIs are public package exports, but their contracts may evolve more quickly.
**Reference Reference snippet**
```ts
import {
getApiStability,
TAVO_API_STABILITY
} from "@tavojs/core";
console.log(getApiStability("@tavojs/core/router"));
console.log(TAVO_API_STABILITY["@tavojs/core/dev"]);
```
- Stable: root, router, server, config, plugin, server-only, and the JSX runtime paths.
- Experimental: the consolidated dev entry point.
- Use `getApiStability` for tooling and version-aware diagnostics instead of copying this classification into application code.
- Only `package.json` exports are public; source-internal paths have no compatibility guarantee.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Tavo.js glossary
> Look up the framework terms used throughout Tavo.js guides and API references.
Canonical page: https://tavojs.dev/docs/core/glossary
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- No project setup is required; this is a terminology reference.
## Outcomes
- Interpret Tavo.js-specific routing, rendering, state, and extension terminology.
## Application and routing terms
Term
Meaning in Tavo.js
**Route**
A public URL pattern discovered from a non-underscore module in src/pages.
**Page module**
The file that supplies a route component and optional loader, action, middleware, metadata, and render policy.
**Layout**
A \_layout.tsx module that wraps matching descendant routes and may own its own loader, middleware, and metadata.
**Route group**
A folder in parentheses that organizes routes or layouts without adding a URL segment.
**Loader**
Request-owned read operation that resolves data before a route or layout renders.
**Middleware**
Ordered route work that either continues resolution or redirects before loaders run.
**Action**
A server handler for a non-GET request to a route, normally used for mutations and forms.
**Hydration**
Attaching Tavo.js's browser runtime to server-rendered HTML using the same serialized route state.
## Rendering terms
Term
Expanded form
Meaning in Tavo.js
**CSR**
Client-Side Rendering
The server sends a document shell and the browser resolves and renders the route.
**SSR**
Server-Side Rendering
Tavo.js resolves and renders the route for an incoming request, then the browser hydrates it.
**SSG**
Static Site Generation
Tavo.js prerenders selected SSR routes during the production build.
**ISR**
Incremental Static Regeneration
A runtime caches SSR output and refreshes it after the route's revalidation interval.
**Deferred boundary**
—
An SSR streaming boundary that sends fallback content before optional promise-backed content settles.
## State and extension terms
Term
Meaning in Tavo.js
**Model**
Reactive state owned by one mounted createTavo component.
**Controller**
Component behavior, lifecycle, services, and managed side effects in createTavo.
**Store**
Observable state container; a global store is for browser state shared by multiple consumers, not request identity.
**Resource**
Component-owned asynchronous read state with loading, success, error, reset, and cancellation behavior.
**Plugin**
A manifest-backed framework integration with lazy client, server, and build phase implementations.
**Server-only module**
A module Tavo.js prevents from entering the client bundle, normally under src/server or marked with the server-only import.
## Look up exact public types
**Public API names are navigable**
Linked API names throughout this guide open the canonical Core declaration with its complete signature, runtime boundary, stability, import path, and owning guide.
[Browse every public Core symbol](/docs/core/api)
# Core API reference
> Choose the stable root, router, server, config, plugin, and development entry points intentionally.
Canonical page: https://tavojs.dev/docs/core/api
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: browser, server, build
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- The installed @tavojs/core package whose exports you need to inspect.
## Outcomes
- Choose the stable root, router, server, config, plugin, and development entry points intentionally.
## Import from the narrowest public entry point
The root package provides common application APIs. Use the five memorable feature boundaries for routing, server work, configuration, plugins, and development tooling.
**Reference Public import map — copy only the imports you use**
```ts
import { createResource, createStore, createTavo, Seo } from "@tavojs/core";
import { defineRoutePage, navigate } from "@tavojs/core/router";
import { createSessionStorage } from "@tavojs/core/server";
import { defineConfig } from "@tavojs/core/config";
import { definePlugin } from "@tavojs/core/plugin";
import { createPagesTestHarness } from "@tavojs/core/dev";
```
## Use entry points by responsibility
- `@tavojs/core`: components, JSX, stores, actions, forms, resources, styling, localization, and common application APIs.
- `@tavojs/core/router`: Auto Pages routes, navigation, loaders, actions, middleware, subscriptions, and route types.
- `@tavojs/core/server`: Node SSR, document rendering, sessions, and server utilities.
- `@tavojs/core/config` and `@tavojs/core/plugin`: project configuration and Plugin API v1.
- `@tavojs/core/dev`: experimental inspection, testing, instrumentation, validation, scheduling, and devtools.
- `@tavojs/core/server-only` and the JSX runtime paths are technical integration boundaries.
## Treat declarations as the exact contract
Guides explain how APIs fit together; generated `TypeScript` declarations remain authoritative for overloads, generic parameters, and return values in the installed version.
**Do not import source internals**
Only package.json exports are public. Internal file paths can change without a public API migration path.
## Rendering and production entry points
API / option
Type
Default
Behavior
`@tavojs/core`
`stable`
symbol-specific
Common application APIs. DOM, focus, ref, directive, and transition symbols are browser-only; JSX values, static rendering, and style registries also work on the server.
`@tavojs/core/router`
`stable`
browser + server
Auto Pages routing, navigation, route modules, loaders, actions, middleware, subscriptions, and route types.
`@tavojs/core/server`
`stable`
server
Node SSR, document rendering, sessions, and server utilities.
`@tavojs/core/config`
`stable`
build
Configuration authoring helpers, exact project types, and Vite integration.
`@tavojs/core/plugin`
`stable`
build + server + browser
Plugin-author declarations, manifests, phases, permissions, capabilities, stores, and compatibility helpers.
`@tavojs/core/dev`
`experimental`
development + tests
Inspection, testing, instrumentation, validation, scheduling, devtools, configuration loading, overlays, and development servers.
`@tavojs/core/server-only`
`stable`
server marker
Empty side-effect boundary that rejects execution in a browser bundle.
**Declarations are authoritative**
These guides explain behavior and defaults. Use the declarations shipped with the installed package for exact generic parameters, overloads, and return types, and import only package.json exports.
## Browse by responsibility
[
## Components, JSX, and DOM
35 public exports`@tavojs/core`
Component contracts, JSX output, roots, refs, directives, focus, observers, and transitions from the root package.
Read guide →
](/docs/core/api/components-and-dom)[
## Errors and code splitting
12 public exports`@tavojs/core`
Error boundaries and lazy component contracts for pending, failure, and loaded states.
Read guide →
](/docs/core/api/errors-and-code-splitting)[
## Runtime contracts
16 public exports`@tavojs/core`
Shared state helpers, storage and unsubscribe contracts, diagnostics, and API stability metadata.
Read guide →
](/docs/core/api/runtime-contracts)[
## MVC, stores, and services
36 public exports`@tavojs/core`
createTavo, controllers, application boot, services, stores, persistence, and selectors.
Read guide →
](/docs/core/api/application)[
## Data, actions, and async
26 public exports`@tavojs/core`
Actions, forms, resources, deferred values, and their data contracts.
Read guide →
](/docs/core/api/data-actions-and-async)[
## Localization, SEO, and assets
44 public exports`@tavojs/core`
Localization state and routing, document head, SEO metadata, images, fonts, scripts, and style registries.
Read guide →
](/docs/core/api/localization-seo-and-assets)[
## Router and Auto Pages
52 public exports`@tavojs/core/router`
Routes, navigation, loaders, middleware, actions, route modules, status, prefetching, and subscriptions.
Read guide →
](/docs/core/api/router)[
## Server rendering and sessions
27 public exports`@tavojs/core/server`
Node request handling, rendering, static caches, image optimization, environment loading, and sessions.
Read guide →
](/docs/core/api/server)[
## Configuration
8 public exports`@tavojs/core/config`
Stable authoring helpers and exact project and Vite configuration contracts.
Read guide →
](/docs/core/api/config)[
## Plugin API
46 public exports`@tavojs/core/plugin`
Plugin-author declarations, manifests, permissions, capabilities, phases, stores, and compatibility helpers.
Read guide →
](/docs/core/api/plugin)[
## Development and testing
55 public exports`@tavojs/core/dev`
Experimental testing, validation, diagnostics, instrumentation, scheduling, inspection, overlays, configuration loading, and development servers.
Read guide →
](/docs/core/api/dev)
# Components, JSX, and DOM API
> Component contracts, JSX output, roots, refs, directives, focus, observers, and transitions from the root package.
Canonical page: https://tavojs.dev/docs/core/api/components-and-dom
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- The installed @tavojs/core package whose declarations you need to inspect.
## Outcomes
- Locate and import every public components, jsx, and dom contract from its canonical package boundary.
## Components, JSX, and DOM exports
### @tavojs/core
Canonical import boundary for every symbol in this section.
#### autoFocus[#](#api-tavojs-core--autofocus)
```
autoFocus(options?: FocusOptions | undefined): ElementDirective
```
Creates a directive that focuses the element after it is mounted.
[Related guide](/docs/core/dom-and-accessibility#directives)
#### captureFocusRestore[#](#api-tavojs-core--capturefocusrestore)
```
captureFocusRestore(documentRef?: Document | undefined): () => void
```
Captures current focus and returns a function that restores it later.
[Related guide](/docs/core/dom-and-accessibility#focus-ownership)
#### Child[#](#api-tavojs-core--child)
```
type Child = Primitive | VNode | Child[];
```
Defines the child contract used by the application runtime.
[Related guide](/docs/core/components-and-jsx#functional-components)
#### ClassName[#](#api-tavojs-core--classname)
```
type ClassName = string | string[];
```
Defines the class name contract used by the application runtime.
[Related guide](/docs/core/components-and-jsx#intrinsic-runtime)
#### Component[#](#api-tavojs-core--component)
```
type Component
= Record> = (props: PropsWithChildren
) => Child;
```
Defines the component contract used by the application runtime.
[Related guide](/docs/core/components-and-jsx#functional-components)
#### createDirective[#](#api-tavojs-core--createdirective)
```
createDirective(directive: ElementDirective): ElementDirective
```
Creates a reusable element directive from a function.
[Related guide](/docs/core/dom-and-accessibility#directives)
#### createListRefs[#](#api-tavojs-core--createlistrefs)
```
createListRefs(): { get(key: K): DomRefObject; delete(key: K): boolean; clear(): void; entries(): IterableIterator<[K, DomRefObject]>; }
```
Creates a keyed collection of refs for dynamic lists.
[Related guide](/docs/core/dom-and-accessibility#ref-lifecycle)
#### createRef[#](#api-tavojs-core--createref)
```
createRef(): DomRefObject
```
Creates a mutable DOM ref object for controller-owned element access.
[Related guide](/docs/core/dom-and-accessibility#ref-lifecycle)
#### createRoot[#](#api-tavojs-core--createroot)
```
createRoot(container: Element | DocumentFragment): Root
```
Creates root for the application runtime.
[Related guide](/docs/core/components-and-jsx#manual-rendering)
#### DomRef[#](#api-tavojs-core--domref)
```
type DomRef = DomRefObject | DomRefCallback | null | undefined;
```
Public DOM ref value accepted by intrinsic JSX elements.
[Related guide](/docs/core/dom-and-accessibility#ref-lifecycle)
#### DomRefCallback[#](#api-tavojs-core--domrefcallback)
```
type DomRefCallback = (node: T | null) => void;
```
Callback ref shape for one-off DOM element access.
[Related guide](/docs/core/dom-and-accessibility#ref-lifecycle)
#### DomRefObject[#](#api-tavojs-core--domrefobject)
```
type DomRefObject = {
current: T | null;
};
```
Object ref shape used by MVC controllers to keep direct DOM handles.
[Related guide](/docs/core/dom-and-accessibility#ref-lifecycle)
#### ElementCleanup[#](#api-tavojs-core--elementcleanup)
```
type ElementCleanup = () => void;
```
Defines the element cleanup contract used by the application runtime.
[Related guide](/docs/core/dom-and-accessibility#directives)
#### ElementDirective[#](#api-tavojs-core--elementdirective)
```
type ElementDirective = (element: T) => void | ElementCleanup;
```
Defines the element directive contract used by the application runtime.
[Related guide](/docs/core/dom-and-accessibility#directives)
#### ElementDirectiveInput[#](#api-tavojs-core--elementdirectiveinput)
```
type ElementDirectiveInput = ElementDirective | Array | null | undefined | false> | null | undefined | false;
```
Defines the element directive input contract used by the application runtime.
[Related guide](/docs/core/dom-and-accessibility#directives)
#### ElementTarget[#](#api-tavojs-core--elementtarget)
```
type ElementTarget = T | DomRefObject;
```
Defines the element target contract used by the application runtime.
[Related guide](/docs/core/dom-and-accessibility#managed-observers)
#### focusFirst[#](#api-tavojs-core--focusfirst)
```
focusFirst(root: ParentNode, options?: FocusOptions | undefined): HTMLElement | null
```
Focuses the first focusable descendant inside a root node.
[Related guide](/docs/core/dom-and-accessibility#focus-ownership)
#### focusFirstInvalid[#](#api-tavojs-core--focusfirstinvalid)
```
focusFirstInvalid(root: ParentNode, options?: FocusOptions | undefined): HTMLElement | null
```
Focuses the first invalid form control inside a root node.
[Related guide](/docs/core/dom-and-accessibility#focus-ownership)
#### Fragment[#](#api-tavojs-core--fragment)
```
Fragment: typeof Fragment
```
Provides fragment behavior for the application runtime.
[Related guide](/docs/core/components-and-jsx#functional-components)
#### getFocusableElements[#](#api-tavojs-core--getfocusableelements)
```
getFocusableElements(root: ParentNode): HTMLElement[]
```
Finds focusable descendants in DOM order.
[Related guide](/docs/core/dom-and-accessibility#focus-ownership)
#### h[#](#api-tavojs-core--h)
```
h(type: NodeType, props: (Record & { children?: Child }) | null, ...children: Child[]): VNode
```
Provides h behavior for the application runtime.
[Related guide](/docs/core/components-and-jsx#intrinsic-runtime)
#### mergeRefs[#](#api-tavojs-core--mergerefs)
```
mergeRefs(...refs: DomRef[]): DomRefCallback
```
Combines several refs into one callback ref.
[Related guide](/docs/core/dom-and-accessibility#ref-lifecycle)
#### observeIntersection[#](#api-tavojs-core--observeintersection)
```
observeIntersection(target: ElementTarget, listener: IntersectionObserverCallback, options?: IntersectionObserverInit | undefined): Unsubscribe
```
Observes element viewport intersection changes and returns an unsubscribe function.
[Related guide](/docs/core/dom-and-accessibility#managed-observers)
#### observeMutation[#](#api-tavojs-core--observemutation)
```
observeMutation(target: T | { current: T | null; }, listener: MutationCallback, options?: MutationObserverInit | undefined): Unsubscribe
```
Observes DOM mutations and returns an unsubscribe function.
[Related guide](/docs/core/dom-and-accessibility#managed-observers)
#### observeResize[#](#api-tavojs-core--observeresize)
```
observeResize(target: ElementTarget, listener: ResizeObserverCallback, options?: ResizeObserverOptions | undefined): Unsubscribe
```
Observes element size changes and returns an unsubscribe function.
[Related guide](/docs/core/dom-and-accessibility#managed-observers)
#### PropsWithChildren[#](#api-tavojs-core--propswithchildren)
```
type PropsWithChildren
= Record> = P & {
children?: Child;
};
```
Defines the props with children contract used by the application runtime.
[Related guide](/docs/core/components-and-jsx#functional-components)
#### render[#](#api-tavojs-core--render)
```
render(node: Child, container: Element | DocumentFragment): void
```
Renders for the application runtime.
[Related guide](/docs/core/components-and-jsx#manual-rendering)
#### renderToString[#](#api-tavojs-core--rendertostring)
```
renderToString(node: Child): string
```
Renders to string for the application runtime.
[Related guide](/docs/core/components-and-jsx#manual-rendering)
#### Root[#](#api-tavojs-core--root)
```
type Root = {
render(node: Child): void;
hydrate(node: Child): void;
unmount(): void;
};
```
Defines the root contract used by the application runtime.
[Related guide](/docs/core/components-and-jsx#manual-rendering)
#### setRef[#](#api-tavojs-core--setref)
```
setRef(ref: DomRef, node: T | null): void
```
Sets a ref to a DOM node or null. Useful when writing framework adapters.
[Related guide](/docs/core/dom-and-accessibility#ref-lifecycle)
#### transition[#](#api-tavojs-core--transition)
```
transition(options?: TransitionOptions | undefined): ElementDirective
```
Creates a small class/callback transition directive for mounted elements.
[Related guide](/docs/core/dom-and-accessibility#directives)
#### TransitionClassNames[#](#api-tavojs-core--transitionclassnames)
```
type TransitionClassNames = {
enter?: string;
enterActive?: string;
leave?: string;
leaveActive?: string;
};
```
Defines the transition class names contract used by the application runtime.
[Related guide](/docs/core/dom-and-accessibility#directives)
#### TransitionOptions[#](#api-tavojs-core--transitionoptions)
```
type TransitionOptions = {
classes?: TransitionClassNames;
onEnter?: (element: T) => void;
onLeave?: (element: T) => void;
};
```
Configures transition in the application runtime.
[Related guide](/docs/core/dom-and-accessibility#directives)
#### trapFocus[#](#api-tavojs-core--trapfocus)
```
trapFocus(root: HTMLElement): () => void
```
Keeps Tab navigation inside a container until the returned cleanup runs.
[Related guide](/docs/core/dom-and-accessibility#focus-ownership)
#### VNode[#](#api-tavojs-core--vnode)
```
type VNode = {
type: NodeType;
props: {
children: Child[];
[key: string]: unknown;
};
};
```
Defines the v node contract used by the application runtime.
[Related guide](/docs/core/components-and-jsx#intrinsic-runtime)
# Errors and code splitting API
> Error boundaries and lazy component contracts for pending, failure, and loaded states.
Canonical page: https://tavojs.dev/docs/core/api/errors-and-code-splitting
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- The installed @tavojs/core package whose declarations you need to inspect.
## Outcomes
- Locate and import every public errors and code splitting contract from its canonical package boundary.
## Errors and code splitting exports
### @tavojs/core
Canonical import boundary for every symbol in this section.
#### ErrorBoundary[#](#api-tavojs-core--errorboundary)
```
ErrorBoundary(props: ErrorBoundaryProps): ErrorBoundaryVNode
```
Creates an error boundary vnode that captures descendant render errors.
[Related guide](/docs/core/errors-and-code-splitting#error-boundary)
#### ErrorBoundaryProps[#](#api-tavojs-core--errorboundaryprops)
```
type ErrorBoundaryProps = {
children?: Child;
fallback: ErrorBoundaryFallback;
resetKey?: unknown;
};
```
Defines the props accepted by error boundary in the application runtime.
[Related guide](/docs/core/errors-and-code-splitting#error-boundary)
#### lazy[#](#api-tavojs-core--lazy)
```
lazy
```
Creates a component that loads its implementation with a dynamic import on first render.
[Related guide](/docs/core/errors-and-code-splitting#lazy-components)
#### LazyComponent[#](#api-tavojs-core--lazycomponent)
```
type LazyComponent
> = Component
& {
preload(): Promise>;
getStatus(): LazyStatus
;
};
```
Defines the lazy component contract used by the application runtime.
[Related guide](/docs/core/errors-and-code-splitting#lazy-components)
#### LazyErrorFallback[#](#api-tavojs-core--lazyerrorfallback)
```
type LazyErrorFallback = Child | ((state: LazyErrorState) => Child);
```
Defines the lazy error fallback contract used by the application runtime.
[Related guide](/docs/core/errors-and-code-splitting#lazy-components)
#### LazyErrorState[#](#api-tavojs-core--lazyerrorstate)
```
type LazyErrorState = {
status: "error";
error: unknown;
};
```
Represents the observable state of lazy error in the application runtime.
[Related guide](/docs/core/errors-and-code-splitting#lazy-components)
#### LazyFallback[#](#api-tavojs-core--lazyfallback)
```
type LazyFallback = Child | ((state: LazyPendingState) => Child);
```
Defines the lazy fallback contract used by the application runtime.
[Related guide](/docs/core/errors-and-code-splitting#lazy-components)
#### LazyLoader[#](#api-tavojs-core--lazyloader)
```
type LazyLoader
> = () => Promise>;
```
Defines the lazy loader contract used by the application runtime.
[Related guide](/docs/core/errors-and-code-splitting#lazy-components)
#### LazyModule[#](#api-tavojs-core--lazymodule)
```
type LazyModule
> = Component
| {
default: Component
;
};
```
Defines the lazy module contract used by the application runtime.
[Related guide](/docs/core/errors-and-code-splitting#lazy-components)
#### LazyOptions[#](#api-tavojs-core--lazyoptions)
```
type LazyOptions = {
fallback?: LazyFallback;
errorFallback?: LazyErrorFallback;
};
```
Configures lazy in the application runtime.
[Related guide](/docs/core/errors-and-code-splitting#lazy-components)
#### LazyPendingState[#](#api-tavojs-core--lazypendingstate)
```
type LazyPendingState = {
status: "idle" | "loading";
};
```
Represents the observable state of lazy pending in the application runtime.
[Related guide](/docs/core/errors-and-code-splitting#lazy-components)
#### LazyStatus[#](#api-tavojs-core--lazystatus)
```
type LazyStatus
;
error: null;
} | {
status: "error";
component: null;
error: unknown;
};
```
Defines the lazy status contract used by the application runtime.
[Related guide](/docs/core/errors-and-code-splitting#lazy-components)
# Runtime contracts API
> Shared state helpers, storage and unsubscribe contracts, diagnostics, and API stability metadata.
Canonical page: https://tavojs.dev/docs/core/api/runtime-contracts
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- The installed @tavojs/core package whose declarations you need to inspect.
## Outcomes
- Locate and import every public runtime contracts contract from its canonical package boundary.
## Runtime contracts exports
### @tavojs/core
Canonical import boundary for every symbol in this section.
#### ApiStability[#](#api-tavojs-core--apistability)
```
type ApiStability = "stable" | "experimental";
```
Defines the api stability contract used by the application runtime.
[Related guide](/docs/core/api-stability#stability-contract)
#### ApiStabilityEntry[#](#api-tavojs-core--apistabilityentry)
```
type ApiStabilityEntry = {
level: ApiStability;
since: string;
note: string;
};
```
Describes one entry in api stability in the application runtime.
[Related guide](/docs/core/api-stability#stability-contract)
#### formatTavoError[#](#api-tavojs-core--formattavoerror)
```
formatTavoError(error: TavoError): string
```
Produces a human-readable diagnostic while preserving the stable code for logs and tooling.
[Related guide](/docs/core/diagnostics#coded-errors)
#### getApiStability[#](#api-tavojs-core--getapistability)
```
getApiStability(entryPoint: "@tavojs/core" | "@tavojs/core/config" | "@tavojs/core/dev" | "@tavojs/core/jsx-dev-runtime" | "@tavojs/core/jsx-runtime" | "@tavojs/core/plugin" | "@tavojs/core/router" | "@tavojs/core/server" | "@tavojs/core/server-only"): ApiStabilityEntry
```
Reads api stability for the application runtime.
[Related guide](/docs/core/api-stability#stability-contract)
#### isTavoError[#](#api-tavojs-core--istavoerror)
```
isTavoError(error: unknown): error is TavoError
```
Reports whether the current value satisfies tavo error for the application runtime.
[Related guide](/docs/core/diagnostics#coded-errors)
#### shallowEqual[#](#api-tavojs-core--shallowequal)
```
shallowEqual(left: unknown, right: unknown): boolean
```
Provides shallow equal behavior for the application runtime.
[Related guide](/docs/core/stores#store-subscriptions)
#### StatePatch[#](#api-tavojs-core--statepatch)
```
type StatePatch> = Partial | ((previous: T) => Partial);
```
Defines the state patch contract used by the application runtime.
[Related guide](/docs/core/stores#store-contract)
#### StateUpdater[#](#api-tavojs-core--stateupdater)
```
type StateUpdater> = T | ((previous: T) => T);
```
Defines the state updater contract used by the application runtime.
[Related guide](/docs/core/stores#store-contract)
#### StorageLike[#](#api-tavojs-core--storagelike)
```
type StorageLike = {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem?(key: string): void;
};
```
Defines the storage like contract used by the application runtime.
[Related guide](/docs/core/stores#derived-and-persistent-stores)
#### TAVO\_API\_STABILITY[#](#api-tavojs-core--tavo-api-stability)
```
TAVO_API_STABILITY: Readonly<{ "@tavojs/core": { level: "stable"; since: string; note: string; }; "@tavojs/core/config": { level: "stable"; since: string; note: string; }; "@tavojs/core/dev": { level: "experimental"; since: string; note: string; }; "@tavojs/core/jsx-dev-runtime": { level: "stable"; since: string; note: string; }; "@tavojs/core/jsx-runtime": { level: "stable"; since: string; note: string; }; "@tavojs/core/plugin": { level: "stable"; since: string; note: string; }; "@tavojs/core/router": { level: "stable"; since: string; note: string; }; "@tavojs/core/server": { level: "stable"; since: string; note: string; }; "@tavojs/core/server-only": { level: "stable"; since: string; note: string; }; }>
```
Machine-readable stability contract for public package entry points. Stable entry points follow semantic versioning from Tavo.js 1.0 onward.
[Related guide](/docs/core/api-stability#stability-contract)
#### TAVO\_DIAGNOSTIC\_MESSAGES[#](#api-tavojs-core--tavo-diagnostic-messages)
```
TAVO_DIAGNOSTIC_MESSAGES: Readonly<{ readonly TAVO_PAGES_001: "The resolved page cache limit is invalid."; readonly TAVO_PAGES_002: "The client root element is missing."; readonly TAVO_PAGES_003: "The auto-discovery pattern is unsupported."; readonly TAVO_PAGES_004: "The bundler page-discovery API is unavailable."; readonly TAVO_PAGES_005: "Server bootstrap is missing page modules."; readonly TAVO_PAGES_006: "A page module declares conflicting static generation options."; readonly TAVO_SSR_001: "The canonical SSR origin is invalid."; readonly TAVO_CONFIG_001: "A server-only module reached the client bundle."; readonly TAVO_CONFIG_002: "A likely secret environment value is referenced by client code."; readonly TAVO_PLUGIN_001: "A plugin targets an unsupported Tavo.js plugin API version."; readonly TAVO_PLUGIN_002: "A plugin manifest or identity is invalid."; readonly TAVO_PLUGIN_003: "Plugin ownership or contribution is duplicated."; readonly TAVO_PLUGIN_004: "A plugin dependency or capability requirement is not satisfied."; readonly TAVO_PLUGIN_005: "A plugin dependency or ordering graph contains a cycle."; readonly TAVO_PLUGIN_006: "A plugin requested a reserved resource or missing permission."; readonly TAVO_PLUGIN_007: "A plugin phase does not implement its declared manifest."; readonly TAVO_PLUGIN_008: "A plugin failed during initialization or build."; readonly TAVO_PLUGIN_009: "A plugin failed while handling or disposing a request."; readonly TAVO_HYDRATION_001: "Strict hydration detected a server/client mismatch."; }>
```
Exposes the tavo diagnostic messages constant used by the application runtime.
[Related guide](/docs/core/diagnostics#coded-errors)
#### TavoDiagnosticCode[#](#api-tavojs-core--tavodiagnosticcode)
```
type TavoDiagnosticCode = keyof typeof TAVO_DIAGNOSTIC_MESSAGES;
```
Defines the tavo diagnostic code contract used by the application runtime.
[Related guide](/docs/core/diagnostics#coded-errors)
#### TavoError[#](#api-tavojs-core--tavoerror)
```
class TavoError extends Error {
readonly code: TavoDiagnosticCode;
readonly details?: Readonly>;
readonly hint?: string;
constructor(code: TavoDiagnosticCode, message?: string, options?: TavoErrorOptions);
}
```
Framework error with a stable code and optional structured remediation context.
[Related guide](/docs/core/diagnostics#coded-errors)
#### TavoErrorOptions[#](#api-tavojs-core--tavoerroroptions)
```
type TavoErrorOptions = {
cause?: unknown;
details?: Readonly>;
hint?: string;
};
```
Configures tavo error in the application runtime.
[Related guide](/docs/core/diagnostics#coded-errors)
#### TavoPublicEntryPoint[#](#api-tavojs-core--tavopublicentrypoint)
```
type TavoPublicEntryPoint = keyof typeof TAVO_API_STABILITY;
```
Defines the tavo public entry point contract used by the application runtime.
[Related guide](/docs/core/api-stability#stability-contract)
#### Unsubscribe[#](#api-tavojs-core--unsubscribe)
```
type Unsubscribe = () => void;
```
Defines the unsubscribe contract used by the application runtime.
[Related guide](/docs/core/mvc#lifecycle-and-cleanup)
# MVC, stores, and services API
> createTavo, controllers, application boot, services, stores, persistence, and selectors.
Canonical page: https://tavojs.dev/docs/core/api/application
Documentation snapshot: 71f450c246210d6878a335e9ffb349f023bd8931bc88d870a3bbd2a0303de8f2
Content type: reference
Runtime: browser, server
Package versions: tavo-framework 1.0.2, tavo-cli 1.0.1, tavo-ui 1.0.1
## Prerequisites
- The installed @tavojs/core package whose declarations you need to inspect.
## Outcomes
- Locate and import every public mvc, stores, and services contract from its canonical package boundary.
## MVC, stores, and services exports
### @tavojs/core
Canonical import boundary for every symbol in this section.
#### bootTavo[#](#api-tavojs-core--boottavo)
```
bootTavo(options?: BootTavoOptions | undefined): Promise
```
Boots the default Tavo.js app behavior for projects that use file-based pages.
[Related guide](/docs/core/ssr-and-hydration#boot-and-hydration-reference)
#### BootTavoOptions[#](#api-tavojs-core--boottavooptions)
```
type BootTavoOptions = BootTavoClientOptions & BootTavoServerOptions;
```
Configures boot tavo in the application runtime.
[Related guide](/docs/core/ssr-and-hydration#boot-and-hydration-reference)
#### BootTavoResult[#](#api-tavojs-core--boottavoresult)
```
type BootTavoResult = {
mode: "client";
root: Root;
} | {
mode: "server";
handle: ReturnType;
modules: PageModules;
} | {
mode: "none";
};
```
Describes the result returned by boot tavo in the application runtime.
[Related guide](/docs/core/ssr-and-hydration#boot-and-hydration-reference)
#### computedStore[#](#api-tavojs-core--computedstore)
```
computedStore, S extends Record>(source: Store, selector: StoreSelector, options?: { isEqual?: ((left: S, right: S) => boolean) | undefined; } | undefined): Store
```
Creates a derived readonly store that updates whenever the source store's selected value changes.
[Related guide](/docs/core/stores#external-stores)
#### createExternalStore[#](#api-tavojs-core--createexternalstore)
```
createExternalStore(store: ExternalStore): ExternalStore
```
Creates external store for the application runtime.
[Related guide](/docs/core/stores#external-stores)
#### createServiceKey[#](#api-tavojs-core--createservicekey)
```
createServiceKey(name: string): ServiceKey
```
Creates a typed key for registering and resolving a named service.
[Related guide](/docs/core/services-and-dependencies#typed-services)
#### createStore[#](#api-tavojs-core--createstore)
```
createStore>(initialState: T | StoreInitializer): Store
```
Creates store for the application runtime.
[Related guide](/docs/core/stores#store-contract)
#### createTavo[#](#api-tavojs-core--createtavo)
```
createTavo
(definition: MvcComponentDefinition
): Component
```
Creates tavo for the application runtime.
[Related guide](/docs/core/mvc#definition-contract)
#### defineGlobalStore[#](#api-tavojs-core--defineglobalstore)
```
defineGlobalStore(name: string, initialState: T | StoreInitializer): Store
```
Defines a named global store once and returns the shared instance.
[Related guide](/docs/core/stores#global-stores)
#### ExternalStore[#](#api-tavojs-core--externalstore)
```
type ExternalStore = {
getSnapshot(): T;
getServerSnapshot?: () => T;
subscribe(listener: () => void): Unsubscribe;
};
```
Defines storage behavior for external in the application runtime.
[Related guide](/docs/core/stores#external-stores)
#### getGlobalStore[#](#api-tavojs-core--getglobalstore)
```
getGlobalStore(name: string): Store
```
Looks up a previously defined global store by name.
[Related guide](/docs/core/stores#global-stores)
#### getService[#](#api-tavojs-core--getservice)
```
getService(identifier: ServiceIdentifier): T
```
Looks up a previously registered service by name.
[Related guide](/docs/core/services-and-dependencies#selection)
#### getTavoBootMode[#](#api-tavojs-core--gettavobootmode)
```
getTavoBootMode(options?: Pick | undefined): TavoBootMode
```
Returns the boot mode Tavo.js will use for the current document.
[Related guide](/docs/core/ssr-and-hydration#boot-and-hydration-reference)
#### hasGlobalStore[#](#api-tavojs-core--hasglobalstore)
```
hasGlobalStore(name: string): boolean
```
Returns true when a named global store exists in the shared registry.
[Related guide](/docs/core/stores#global-stores)
#### hasService[#](#api-tavojs-core--hasservice)
```
hasService(identifier: ServiceIdentifier): boolean
```
Returns true when a named service exists in the shared registry.
[Related guide](/docs/core/services-and-dependencies#selection)
#### listGlobalStores[#](#api-tavojs-core--listglobalstores)
```
listGlobalStores(): string[]
```
Lists all registered global store names.
[Related guide](/docs/core/stores#global-stores)
#### listServices[#](#api-tavojs-core--listservices)
```
listServices(): string[]
```
Lists all registered service names.
[Related guide](/docs/core/services-and-dependencies#selection)
#### MvcComponentDefinition[#](#api-tavojs-core--mvccomponentdefinition)
```
type MvcComponentDefinition
= {
model?: (props: P) => S | Store;
controller?: new (ctx: MvcControllerContext