+ {children}
+
+
+ );
+}
diff --git a/.docs/site/app/page.tsx b/.docs/site/app/page.tsx
new file mode 100644
index 0000000..6f40eba
--- /dev/null
+++ b/.docs/site/app/page.tsx
@@ -0,0 +1,38 @@
+import Link from "next/link";
+
+const title = "Befter";
+const description = "A lightweight JavaScript library for managing hooks — register, invoke, update, and sequence them with full control over execution order.";
+
+export default function HomePage() {
+ return (
+
+
+
+
+ Documentation
+
+
{title}
+
{description}
+
+ Author markdown in{" "}
+
+ docs/
+
+ . Everything under{" "}
+ /docs is synced from that folder.
+
+
+
+ Open documentation
+
+
+
+
+ );
+}
diff --git a/.docs/site/docs.config.tsx b/.docs/site/docs.config.tsx
new file mode 100644
index 0000000..e4fe3b4
--- /dev/null
+++ b/.docs/site/docs.config.tsx
@@ -0,0 +1,19 @@
+import { defineDocs } from "@farming-labs/docs";
+import { colorful } from "@farming-labs/theme/colorful";
+
+export default defineDocs({
+ entry: "docs",
+ theme: colorful(),
+ ordering: [
+ {
+ "slug": "quickstart"
+ },
+ {
+ "slug": "installation"
+ }
+ ],
+ metadata: {
+ titleTemplate: "%s – Docs",
+ description: "Managed by @farming-labs/docs Cloud",
+ },
+});
diff --git a/.docs/site/next-env.d.ts b/.docs/site/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/.docs/site/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/.docs/site/next.config.ts b/.docs/site/next.config.ts
new file mode 100644
index 0000000..948bed4
--- /dev/null
+++ b/.docs/site/next.config.ts
@@ -0,0 +1,12 @@
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { withDocs } from "@farming-labs/next/config";
+
+const appDir = dirname(fileURLToPath(import.meta.url));
+const root = join(appDir, "../..");
+
+export default withDocs({
+ turbopack: {
+ root,
+ },
+});
diff --git a/.docs/site/package.json b/.docs/site/package.json
new file mode 100644
index 0000000..a3012c4
--- /dev/null
+++ b/.docs/site/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "docs-cloud-managed-runtime",
+ "private": true,
+ "packageManager": "pnpm@10.9.0",
+ "scripts": {
+ "sync:content": "node ./scripts/sync-managed-content.mjs",
+ "dev": "node ./scripts/sync-managed-content.mjs && next dev --turbopack",
+ "build": "node ./scripts/sync-managed-content.mjs && next build --turbopack",
+ "start": "node ./scripts/sync-managed-content.mjs && next start"
+ },
+ "dependencies": {
+ "@farming-labs/docs": "latest",
+ "@farming-labs/next": "latest",
+ "@farming-labs/theme": "latest",
+ "next": "16.2.3",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "zod": "^4.1.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4.1.18",
+ "@types/mdx": "^2.0.13",
+ "@types/node": "^22.10.0",
+ "@types/react": "^19.2.0",
+ "@types/react-dom": "^19.2.0",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^4.1.18",
+ "typescript": "^5.9.3"
+ }
+}
diff --git a/.docs/site/postcss.config.mjs b/.docs/site/postcss.config.mjs
new file mode 100644
index 0000000..61e3684
--- /dev/null
+++ b/.docs/site/postcss.config.mjs
@@ -0,0 +1,7 @@
+const config = {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ },
+};
+
+export default config;
diff --git a/.docs/site/scripts/sync-managed-content.mjs b/.docs/site/scripts/sync-managed-content.mjs
new file mode 100644
index 0000000..bd8e35b
--- /dev/null
+++ b/.docs/site/scripts/sync-managed-content.mjs
@@ -0,0 +1,159 @@
+import { cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
+import { basename, dirname, extname, join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const runtimeRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
+const repoRoot = resolve(runtimeRoot, "../..");
+const authoredRoots = [
+ { source: resolve(repoRoot, "docs"), target: resolve(runtimeRoot, "app/docs") },
+ { source: resolve(repoRoot, "api-reference"), target: resolve(runtimeRoot, "app/docs/api") },
+];
+const ignoredDirectoryNames = new Set([
+ "node_modules",
+ ".next",
+ ".turbo",
+ ".vercel",
+ "dist",
+ "build",
+ "coverage",
+]);
+const ignoredFileNames = new Set([
+ "bun.lock",
+ "jsconfig.json",
+ "package-lock.json",
+ "package.json",
+ "pnpm-lock.yaml",
+ "tsconfig.json",
+ "yarn.lock",
+]);
+const staticAssetExtensions = new Set([
+ ".avif",
+ ".bmp",
+ ".csv",
+ ".gif",
+ ".ico",
+ ".jpeg",
+ ".jpg",
+ ".json",
+ ".mp4",
+ ".pdf",
+ ".png",
+ ".svg",
+ ".txt",
+ ".webm",
+ ".webp",
+ ".zip",
+]);
+const codeFenceLanguageAliases = new Map([
+ ["dotenv", "bash"],
+ ["env", "bash"],
+ ["shell", "bash"],
+]);
+
+function isMarkdownFile(path) {
+ return [".md", ".mdx"].includes(extname(path).toLowerCase());
+}
+
+function isStaticAssetFile(path) {
+ return staticAssetExtensions.has(extname(path).toLowerCase());
+}
+
+function shouldSkipDirectory(name) {
+ return name.startsWith(".") || ignoredDirectoryNames.has(name);
+}
+
+function shouldSkipFile(name) {
+ return name.startsWith(".") || ignoredFileNames.has(name);
+}
+
+function normalizeMarkdownContent(content) {
+ const fenceMarker = String.fromCharCode(96);
+ const codeFencePattern = new RegExp(
+ "(^|\\n)(" + fenceMarker + "{3,})([A-Za-z0-9_+.-]+)([^\\n" + fenceMarker + "]*)",
+ "g",
+ );
+
+ return content.replace(codeFencePattern, (match, prefix, fence, language, rest = "") => {
+ const normalizedLanguage = codeFenceLanguageAliases.get(language.toLowerCase());
+
+ if (!normalizedLanguage) {
+ return match;
+ }
+
+ return prefix + fence + normalizedLanguage + rest;
+ });
+}
+
+function targetPagePath(targetRoot, relativePath) {
+ const withoutExtension = relativePath.replace(/\.mdx?$/i, "");
+ const routeFileName = basename(withoutExtension).toLowerCase();
+ const isIndexPage = routeFileName === "index" || routeFileName === "page";
+ const targetDirectory = isIndexPage ? dirname(withoutExtension) : withoutExtension;
+ return join(targetRoot, targetDirectory === "." ? "" : targetDirectory, "page.mdx");
+}
+
+async function fileExists(path) {
+ try {
+ await readdir(path);
+ return true;
+ } catch {
+ try {
+ await readFile(path, "utf8");
+ return true;
+ } catch {
+ return false;
+ }
+ }
+}
+
+async function syncAuthoredRoot(sourceRoot, targetRoot) {
+ if (!(await fileExists(sourceRoot))) {
+ return;
+ }
+
+ const visit = async (currentSourceDirectory, relativeDirectory = "") => {
+ const entries = await readdir(currentSourceDirectory, { withFileTypes: true });
+
+ for (const entry of entries) {
+ if (entry.name.startsWith(".")) {
+ continue;
+ }
+
+ const sourcePath = join(currentSourceDirectory, entry.name);
+ const relativePath = relativeDirectory ? join(relativeDirectory, entry.name) : entry.name;
+
+ if (entry.isDirectory()) {
+ if (shouldSkipDirectory(entry.name)) {
+ continue;
+ }
+
+ await visit(sourcePath, relativePath);
+ continue;
+ }
+
+ if (!isMarkdownFile(entry.name)) {
+ if (shouldSkipFile(entry.name) || !isStaticAssetFile(entry.name)) {
+ continue;
+ }
+
+ const targetPath = join(targetRoot, relativePath);
+ await mkdir(dirname(targetPath), { recursive: true });
+ await cp(sourcePath, targetPath, { force: true });
+ continue;
+ }
+
+ const targetPath = targetPagePath(targetRoot, relativePath);
+ await mkdir(dirname(targetPath), { recursive: true });
+ await writeFile(targetPath, normalizeMarkdownContent(await readFile(sourcePath, "utf8")), "utf8");
+ }
+ };
+
+ await visit(sourceRoot);
+}
+
+await rm(resolve(runtimeRoot, "app/docs"), { recursive: true, force: true });
+await mkdir(resolve(runtimeRoot, "app/docs"), { recursive: true });
+
+for (const authoredRoot of authoredRoots) {
+ await syncAuthoredRoot(authoredRoot.source, authoredRoot.target);
+}
diff --git a/.docs/site/tsconfig.json b/.docs/site/tsconfig.json
new file mode 100644
index 0000000..247f602
--- /dev/null
+++ b/.docs/site/tsconfig.json
@@ -0,0 +1,42 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "baseUrl": ".",
+ "paths": {
+ "@/*": [
+ "./*"
+ ]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..46527ef
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,19 @@
+# Docs Maintenance Guide
+Use this file as the handoff checklist for future edits to this documentation PR.
+## Source Layout
+- The docs source lives in `docs/`.
+- `docs.json` is the Docs Cloud configuration for publishing, previews, and content roots.
+- The managed runtime lives in `.docs/site`; edit authored markdown at the repo root instead of generated runtime pages under `.docs/site/app/docs`.
+- Keep every page grounded in README content, package metadata, source exports, CLI help, environment examples, or existing docs.
+## Docs Routes
+- /docs - Introduction
+- /docs/installation - Installation
+- /docs/quickstart - Quickstart
+## Editing Guidelines
+- Prefer reader-facing setup, usage, and troubleshooting notes over source inventories.
+- Do not add commands, flags, environment variables, routes, imports, or framework names unless they are present in the repository.
+- If you add or rename a page, keep its frontmatter title and description accurate and make sure the navigation ordering still includes it.
+- Avoid analyzer language such as generated from, source evidence, implementation map, source surface, or detected in files.
+## Verification
+- Build the docs site with `cd .docs/site && pnpm install && pnpm build` before handing off a docs PR.
+- Open `/docs` and at least one generated leaf page to confirm the sidebar and page content match the PR.
diff --git a/docs.json b/docs.json
new file mode 100644
index 0000000..efe3175
--- /dev/null
+++ b/docs.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://docs.farming-labs.dev/schema/docs.json",
+ "version": 1,
+ "docs": {
+ "mode": "frameworkless",
+ "runtime": "nextjs",
+ "root": ".docs/site"
+ },
+ "content": {
+ "docsRoot": "docs",
+ "apiReferenceRoot": "api-reference",
+ "openapi": []
+ },
+ "cloud": {
+ "apiKey": {
+ "env": "DOCS_CLOUD_API_KEY"
+ },
+ "preview": {
+ "enabled": true
+ },
+ "publish": {
+ "mode": "draft-pr",
+ "baseBranch": "main"
+ }
+ }
+}
diff --git a/docs/index.mdx b/docs/index.mdx
new file mode 100644
index 0000000..6ef4215
--- /dev/null
+++ b/docs/index.mdx
@@ -0,0 +1,15 @@
+---
+title: "Introduction"
+description: "A lightweight JavaScript library for managing hooks — register, invoke, update, and sequence them with full control over execution order."
+order: 0
+---
+
+# Introduction
+
+Befter is a lightweight JavaScript library for managing hooks. It gives you a clean API to register hooks by name, invoke them in sequence, update or remove them at runtime, and precisely control whether a hook runs before or after another — all without pulling in a heavy event system.
+
+The core primitive is `createBefter`, which returns a hook registry you own. From there you attach callbacks with `hook`, trigger them with `callHook`, and clean up with `removeHook` or `removeHookItself`. Every function is fully typed, so TypeScript infers the callback signatures from the hook keys you define.
+
+Befter ships with both in-process (local) and Redis-backed storage adapters, so the same hook model scales from a single Node.js process to a distributed system. Serial and parallel execution helpers give you fine-grained control over how a group of hooks is called.
+
+Ready to add Befter to your project? Head to [Installation](/docs/installation) to add the package, or jump straight to [Quickstart](/docs/quickstart) to see a working example in under a minute.
diff --git a/docs/installation.mdx b/docs/installation.mdx
new file mode 100644
index 0000000..008d10c
--- /dev/null
+++ b/docs/installation.mdx
@@ -0,0 +1,57 @@
+---
+title: "Installation"
+description: "Install and configure Befter."
+order: 10
+---
+
+# Installation
+
+Befter is published to npm as `@farming-labs/befter`. You need Node.js 18 or later before you begin.
+
+## Install the package
+
+Choose your package manager:
+
+```bash
+npm install @farming-labs/befter
+```
+
+```bash
+pnpm add @farming-labs/befter
+```
+
+```bash
+yarn add @farming-labs/befter
+```
+
+## Import the library
+
+Once installed, import `createBefter` to create your first hook registry:
+
+```ts
+import { createBefter } from "@farming-labs/befter"
+```
+
+That single import is all you need to get started. The package ships as an ES module with TypeScript declarations included — no extra `@types` package required.
+
+## Verify the install
+
+Create a small smoke-test file and run it with Node.js or your preferred runtime:
+
+```ts
+import { createBefter } from "@farming-labs/befter"
+
+const befter = createBefter({})
+console.log("Befter ready:", typeof befter.hook === "function")
+```
+
+```bash
+node smoke-test.mjs
+# Befter ready: true
+```
+
+If you see `Befter ready: true`, the package is wired up correctly.
+
+## Next steps
+
+With the package installed, head to [Quickstart](/docs/quickstart) to register your first hook and call it.
diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx
new file mode 100644
index 0000000..bc0c826
--- /dev/null
+++ b/docs/quickstart.mdx
@@ -0,0 +1,75 @@
+---
+title: "Quickstart"
+description: "Run Befter for the first time."
+order: 20
+---
+
+# Quickstart
+
+This page walks you through creating a Befter instance, registering a hook, and invoking it — the complete loop in a single file. You should have Befter installed already; if not, see [Installation](/docs/installation) first.
+
+## Create a registry
+
+`createBefter` returns a registry that holds all your named hooks. Pass an empty object to start with a blank slate:
+
+```ts
+import { createBefter } from "@farming-labs/befter"
+
+const befter = createBefter({})
+```
+
+## Register a hook
+
+Use `hook` to attach a callback to a named key. The name is a plain string that becomes the identifier you use to invoke or remove the hook later:
+
+```ts
+befter.hook("user:created", async (user) => {
+ console.log("New user registered:", user.name)
+})
+```
+
+You can register multiple callbacks under the same key — Befter tracks them all and calls them in registration order by default.
+
+## Call the hook
+
+Use `callHook` to invoke every callback registered under a key, passing whatever arguments your callbacks expect:
+
+```ts
+await befter.callHook("user:created", { name: "Alice" })
+// → New user registered: Alice
+```
+
+`callHook` returns a promise that resolves once all callbacks have finished, making it safe to `await` in async workflows.
+
+## Update or remove a hook
+
+You can swap out a callback at runtime with `updateHook`, or tear it down entirely with `removeHook`:
+
+```ts
+// Remove all callbacks for a key
+befter.removeHook("user:created")
+```
+
+Use `removeHookItself` when you have a direct reference to the callback and want to remove only that one entry, leaving any other callbacks on the same key intact.
+
+## Complete example
+
+```ts
+import { createBefter } from "@farming-labs/befter"
+
+const befter = createBefter({})
+
+befter.hook("order:placed", async (order) => {
+ console.log(`Order #${order.id} received`)
+})
+
+befter.hook("order:placed", async (order) => {
+ console.log(`Sending confirmation email to ${order.email}`)
+})
+
+await befter.callHook("order:placed", { id: 42, email: "alice@example.com" })
+// → Order #42 received
+// → Sending confirmation email to alice@example.com
+```
+
+Both callbacks run in sequence under the same key — no extra wiring needed.
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 3ff5faa..ef0d119 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,3 +1,4 @@
packages:
- "apps/*"
- "packages/*"
+ - ".docs/site"