diff --git a/.agents/skills/documentation/SKILL.md b/.agents/skills/documentation/SKILL.md
new file mode 100644
index 0000000..dae5144
--- /dev/null
+++ b/.agents/skills/documentation/SKILL.md
@@ -0,0 +1,109 @@
+---
+name: documentation
+description: "Creates, structures, and reviews technical documentation following the Diátaxis framework (tutorials, how-to guides, reference, and explanation pages). Use when a user needs to write or reorganize docs, structure a tutorial vs. a how-to guide, build reference docs or API documentation, create explanation pages, choose between Diátaxis documentation types, or improve existing documentation structure. Trigger terms include: documentation structure, Diátaxis, tutorials vs how-to guides, organize docs, user guide, reference docs, technical writing."
+metadata:
+ tags: documentation, technical-writing, tutorials, guides, reference, diataxis
+---
+
+## When to use
+
+Use this skill when you need to create, review, or improve technical documentation following the Diátaxis framework. Examples include:
+- Creating user guides
+- API documentation
+- Tutorial content
+- Restructuring existing documentation to better serve different user needs and contexts
+
+## Instructions
+
+Organize documentation into four distinct types — tutorials, how-to guides, reference material, and explanations — each serving different user needs and contexts.
+
+Always ask clarifying questions about the user's context, audience, and goals **before** creating documentation.
+
+---
+
+### Step 1 — Identify the documentation type
+
+Use the following decision checklist based on user signals:
+
+| User signal | Documentation type |
+|---|---|
+| "I'm new to X and want to learn it" / "walk me through" | **Tutorial** |
+| "How do I…?" / "I need to accomplish X" | **How-to guide** |
+| "What are the parameters/options/syntax for X?" | **Reference** |
+| "Why does X work this way?" / "Help me understand X" | **Explanation** |
+
+Quick decision tree:
+- Is the user **learning by doing** for the first time? → Tutorial
+- Do they need to **solve a specific problem** they already understand? → How-to guide
+- Do they need **technical facts** to look up? → Reference
+- Do they want **conceptual background**? → Explanation
+
+---
+
+### Step 2 — Apply type-specific patterns
+
+#### Tutorials (learning-oriented)
+- **Title pattern:** Start with a verb — *"Build your first X"*, *"Create a Y from scratch"*
+- Structure: Goal → Prerequisites → Numbered steps → Immediate verifiable result at each step → Final outcome
+- Minimise explanation; maximise doing
+- Every step must produce a visible, testable result
+- **Validation:** A beginner must be able to complete the tutorial without external help
+
+**Example intro:**
+> *"In this tutorial, you will build a simple REST API using Express. By the end, you will have a running server that responds to GET requests. No prior Express experience is needed."*
+
+---
+
+#### How-to guides (problem-oriented)
+- **Title pattern:** Frame as a task — *"How to configure X"*, *"How to deploy Y to Z"*
+- Structure: Goal statement → Assumptions/prerequisites → Numbered steps → Expected result
+- Assume baseline knowledge; skip conceptual explanations
+- Allow for variation; note alternatives where they exist
+- **Validation:** An experienced user can complete the task without confusion or backtracking
+
+**Example intro:**
+> *"This guide shows how to add JWT authentication to an existing Express app. It assumes you have a working Express server and basic familiarity with middleware."*
+
+---
+
+#### Reference (information-oriented)
+- **Title pattern:** Name the thing — *"Configuration options"*, *"API endpoints"*, *"CLI flags"*
+- Structure: Consistent repeatable format per entry (name → type → default → description → example)
+- State facts; avoid instruction beyond minimal usage examples
+- Keep current; version-stamp if needed
+- **Validation:** A user can look up a specific fact in under 30 seconds without reading surrounding content
+
+**Example entry:**
+> **`timeout`** *(integer, default: `5000`)*
+> Maximum time in milliseconds to wait for a response before the request fails.
+> *Example:* `{ timeout: 3000 }`
+
+---
+
+#### Explanations (understanding-oriented)
+- **Title pattern:** Frame as a concept — *"How X works"*, *"Understanding Y"*, *"Why Z is designed this way"*
+- Structure: Context → Core concept → Alternatives/trade-offs → Higher-level perspective
+- Avoid step-by-step instruction or technical specification
+- **Validation:** After reading, the user can explain the concept in their own words and understands the rationale behind design decisions
+
+**Example intro:**
+> *"Authentication and authorisation are often confused. This page explains the distinction, why both matter, and how common patterns (sessions, tokens, OAuth) approach each concern differently."*
+
+---
+
+### Step 3 — Maintain separation and integration
+
+- Keep each document a single type — don't mix tutorial steps with reference tables or conceptual digressions
+- Cross-link between types: a tutorial can link to the relevant reference page; a how-to guide can link to an explanation for background
+- Use consistent headings and terminology across all types so users can navigate the full documentation system
+
+---
+
+### Step 4 — Validate before delivering
+
+| Type | Validation check |
+|---|---|
+| Tutorial | Can a beginner complete it end-to-end without external help? |
+| How-to guide | Does it solve the stated problem for an experienced user? |
+| Reference | Can the user find a specific fact in under 30 seconds? |
+| Explanation | Does the user understand the *why*, not just the *what*? |
diff --git a/.agents/skills/documentation/tile.json b/.agents/skills/documentation/tile.json
new file mode 100644
index 0000000..f389cef
--- /dev/null
+++ b/.agents/skills/documentation/tile.json
@@ -0,0 +1,11 @@
+{
+ "name": "mcollina/documentation",
+ "version": "0.1.0",
+ "private": false,
+ "summary": "Creates, structures, and reviews technical documentation following the Diátaxis framework (tutorials, how-to guides, reference, and explanation pages). Use when a user needs to write or reorganize docs, structure a tutorial vs. a how-to guide, build reference docs or API documentation, create explanation pages, choose between Diátaxis documentation types, or improve existing documentation structure. Trigger terms include: documentation structure, Diátaxis, tutorials vs how-to guides, organize docs, user guide, reference docs, technical writing.",
+ "skills": {
+ "documentation": {
+ "path": "SKILL.md"
+ }
+ }
+}
diff --git a/.changeset/docs-readme-localhost.md b/.changeset/docs-readme-localhost.md
new file mode 100644
index 0000000..fe37a55
--- /dev/null
+++ b/.changeset/docs-readme-localhost.md
@@ -0,0 +1,5 @@
+---
+"@vlandoss/env": patch
+---
+
+Slim down package README and move full documentation to the new Fumadocs site
diff --git a/README.md b/README.md
index f62035d..c62f115 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,12 @@
# env
+[](https://www.npmjs.com/package/@vlandoss/env)
+[](https://github.com/variableland/env/actions/workflows/ci.yml)
+[](https://github.com/changesets/changesets)
+
🌱 Contract-first environment configuration with typed schemas and per-runtime entrypoints.
-> Monorepo home for [`@vlandoss/env`](./package) and its examples.
+> Monorepo home for [`@vlandoss/env`](./package), its [examples](./examples), and the [documentation site](./docs).
```bash
pnpm add @vlandoss/env
@@ -32,6 +36,7 @@ This monorepo uses [Turborepo](https://turbo.build/repo/docs):
- `pnpm test` — run unit tests
- `pnpm test:e2e` — run end-to-end tests (Playwright)
- `pnpm test:types` — js & ts checks
+- `pnpm docs` — start the documentation site locally
It also uses [run-run](https://github.com/variableland/dx/blob/main/packages/run-run/README.md) 🦊:
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..58af77e
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,5 @@
+.source
+.tanstack
+.wrangler
+dist
+node_modules
diff --git a/docs/content/docs/api-reference/index.mdx b/docs/content/docs/api-reference/index.mdx
new file mode 100644
index 0000000..7ba5dba
--- /dev/null
+++ b/docs/content/docs/api-reference/index.mdx
@@ -0,0 +1,24 @@
+---
+title: Overview
+description: Every export of @vlandoss/env, organized by the entrypoint you import it from.
+icon: Map
+---
+
+The package is split into five entrypoints. Each page here documents one of them — signature, options, return type, and a minimal usage snippet. For "when to reach for this" prose, follow the link back to the guide that uses it.
+
+## Entrypoints
+
+| Page | Import | Lives in |
+| ----------------------------------------------------- | ------------------------------- | ----------------------------------------- |
+| [Core](/api-reference/core) | `@vlandoss/env` | Any runtime — schema, defineEnv, envName |
+| [Node](/api-reference/node) | `@vlandoss/env/node` | Node — `loadConfig` |
+| [Vite](/api-reference/vite) | `@vlandoss/env/vite` | Vite config — `envConfig()` plugin |
+| [React](/api-reference/react) | `@vlandoss/env/react` | SSR / SSG — `` |
+| [Zod primitives](/api-reference/zod) | `@vlandoss/env/zod` | Any — opinionated single-purpose schemas |
+| [Types](/api-reference/types) | `@vlandoss/env` | All exported type aliases |
+
+## Conventions used in this section
+
+- Tables list options in declaration order.
+- Sync vs. async behavior is marked explicitly on overloaded functions.
+- Each export page ends with a "See also" linking to the relevant guide and concept page.
diff --git a/docs/content/docs/api-reference/meta.json b/docs/content/docs/api-reference/meta.json
new file mode 100644
index 0000000..2e45984
--- /dev/null
+++ b/docs/content/docs/api-reference/meta.json
@@ -0,0 +1,5 @@
+{
+ "title": "API reference",
+ "icon": "Code",
+ "pages": ["index"]
+}
diff --git a/docs/content/docs/concepts/env-name.mdx b/docs/content/docs/concepts/env-name.mdx
new file mode 100644
index 0000000..b2dfce2
--- /dev/null
+++ b/docs/content/docs/concepts/env-name.mdx
@@ -0,0 +1,37 @@
+---
+title: envName()
+description: How @vlandoss/env detects the current environment name across Node, browsers, and Vite builds — including the subtlety with custom modes.
+icon: Hash
+---
+
+`envName()` returns the current environment name. It's the value that drives config-file discovery (`config/.ts`), and it's exposed as `env.$name` on the resolved env object.
+
+## Precedence
+
+First defined wins:
+
+1. **`env.ENV`** — explicit runtime override (always wins). Use this in CI, tests, or scripts.
+2. **`__ENV_NAME__`** — build-time literal injected by the `envConfig()` Vite plugin. Beats `NODE_ENV` because Vite forces `NODE_ENV="production"` regardless of `--mode`, so this is the only way `envName()` can return custom modes like `staging` / `qa` in the browser.
+3. **`env.NODE_ENV`**
+4. **`env.VITE_ENV`**
+5. **`"development"`** — fallback.
+
+## Reading from a specific source
+
+By default, `envName()` reads from `readEnv()` (`process.env` on server, `window.__env` in browser). You can pass an explicit record:
+
+```ts
+import { envName } from "@vlandoss/env";
+
+envName({ NODE_ENV: "test", ENV: "qa" }); // -> "qa"
+```
+
+## The Vite gotcha
+
+If you run `vite build --mode staging`, Vite **still sets `NODE_ENV="production"`** at build time. Without the `envConfig()` plugin, `envName()` in browser code would return `"production"` — not `"staging"`.
+
+The plugin solves this by injecting `__ENV_NAME__ = "staging"` as a build-time constant. That constant wins over `NODE_ENV` in the precedence chain, so the browser sees the mode you actually built.
+
+This is why **the plugin is required for custom Vite modes**, even if you load config via dynamic import (Pattern 1) and don't actually use the `#config` alias.
+
+See [Guides → SPA dynamic import](/guides/spa-dynamic-import) and [Guides → Custom modes](/guides/custom-modes) for the wiring.
diff --git a/docs/content/docs/concepts/env-var-naming.mdx b/docs/content/docs/concepts/env-var-naming.mdx
new file mode 100644
index 0000000..c224654
--- /dev/null
+++ b/docs/content/docs/concepts/env-var-naming.mdx
@@ -0,0 +1,98 @@
+---
+title: Env-var naming
+description: How schema paths become environment variable names, and how to override them with the `vars` option.
+icon: Tag
+---
+
+You don't map every leaf to an env-var name. By convention, each leaf binds to a name derived from its dot-path.
+
+## The convention
+
+camelCase keys are converted to `SCREAMING_SNAKE_CASE` and joined with `_`:
+
+| Schema path | Env var |
+| ----------------------- | ------------------------ |
+| `server.PORT` | `SERVER_PORT` |
+| `db.URL` | `DB_URL` |
+| `sessionCookie.PREFIX` | `SESSION_COOKIE_PREFIX` |
+| `db.kit.LOGGING` | `DB_KIT_LOGGING` |
+| `PORT` (top-level leaf) | `PORT` |
+
+## Per-leaf override
+
+When a leaf doesn't follow the convention, declare just the exception in `vars`:
+
+```ts
+defineEnv({
+ schema: Env,
+ vars: {
+ db: { URL: "DATABASE_URL" }, // DB_URL -> DATABASE_URL
+ },
+});
+```
+
+## Branch prefix shorthand
+
+A string at a branch level becomes the prefix for **every** leaf under it:
+
+```ts
+defineEnv({
+ schema: Env,
+ vars: {
+ db: "DATABASE", // db.URL -> DATABASE_URL, db.LOGGING -> DATABASE_LOGGING
+ },
+});
+```
+
+## Flat branches with `null`
+
+`null` declares a flat branch — leaves under it use their own name with no prefix. Useful when composing schemas where the inner contract already uses fully-qualified names:
+
+```ts
+defineEnv({
+ schema: Env,
+ vars: {
+ server: null, // server.PORT -> PORT, server.HOST -> HOST
+ },
+});
+```
+
+The empty string `""` is **not** accepted — `null` is the only way to declare a flat branch. This avoids the readability confusion of `""` (which looks like a missing value).
+
+## Mixed mode: branch prefix + per-leaf override
+
+Use `$` to set the branch prefix and override individual leaves at the same time:
+
+```ts
+defineEnv({
+ schema: Env,
+ vars: {
+ db: {
+ $: "DATABASE", // branch prefix
+ LOGGING: "POSTGRES_LOGGING", // override for this one leaf
+ },
+ public: {
+ $: null, // flat branch + leaf override
+ APP_NAME: "PUBLIC_APP_NAME",
+ },
+ },
+});
+```
+
+## Type safety
+
+`vars` is fully type-checked against the schema. Unknown branches, unknown leaves, non-uppercase literals, or non-string values fail at compile time:
+
+```ts
+defineEnv({
+ schema: Env,
+ vars: {
+ // @ts-expect-error 'cache' is not in the schema
+ cache: { TTL: "CACHE_TTL" },
+ server: {
+ // @ts-expect-error must be uppercase
+ PORT: "port",
+ },
+ },
+});
+```
diff --git a/docs/content/docs/concepts/meta.json b/docs/content/docs/concepts/meta.json
new file mode 100644
index 0000000..45b8ab0
--- /dev/null
+++ b/docs/content/docs/concepts/meta.json
@@ -0,0 +1,5 @@
+{
+ "title": "Concepts",
+ "icon": "Lightbulb",
+ "pages": ["overview", "resolution", "env-var-naming", "env-name"]
+}
diff --git a/docs/content/docs/concepts/overview.mdx b/docs/content/docs/concepts/overview.mdx
new file mode 100644
index 0000000..1ec8a3b
--- /dev/null
+++ b/docs/content/docs/concepts/overview.mdx
@@ -0,0 +1,66 @@
+---
+title: Overview
+description: The mental model behind @vlandoss/env — one contract, three runtime adapters, and a single resolved env object.
+icon: Map
+---
+
+`@vlandoss/env` is built around four ideas. Once they click, the rest of the API is small.
+
+## 1. One contract per app
+
+A **schema** declares every variable your app expects, grouped into branches. It's the only source of truth — types, validation, and env-var names all derive from it.
+
+```ts
+import { schema } from "@vlandoss/env";
+
+const Env = schema({
+ server: { PORT: z.coerce.number(), HOST: z.string() },
+ db: { URL: z.string() },
+});
+```
+
+Leaves are Standard Schema validators (Zod, Valibot, ArkType…). Branches are nested objects. Other `schema()` results can be inlined to share contracts across files.
+
+## 2. Per-environment config files, not `.env` per environment
+
+Each environment has a file (`development.ts`, `production.ts`, …) that returns a typed object matching the schema. These are **versioned defaults**, not secrets. Secrets still come from `process.env`.
+
+```ts title="src/config/production.ts"
+export default {
+ server: { PORT: 8080, HOST: "0.0.0.0" },
+} satisfies EnvConfig;
+```
+
+## 3. Env vars override config, with predictable naming
+
+At boot, `defineEnv` merges three layers — see [Resolution order](/concepts/resolution). Each leaf maps to an env-var name by convention (`server.PORT` → `SERVER_PORT`); when the convention doesn't fit, override it in `vars` — see [Env-var naming](/concepts/env-var-naming).
+
+## 4. Runtime-agnostic core, opt-in adapters
+
+The core (`@vlandoss/env`) doesn't touch the file system or the bundler. Adapters do:
+
+| Entrypoint | Runtime | Responsibility |
+| ----------------------- | ----------------- | ------------------------------------------------------------- |
+| `@vlandoss/env` | Any | Schema, resolution, validation, types |
+| `@vlandoss/env/node` | Node | Load config files from disk (`loadConfig`) |
+| `@vlandoss/env/vite` | Build time | Alias `#config`, inject `__ENV_NAME__` for the browser |
+| `@vlandoss/env/react` | SSR / SSG | Serialize public env into `