docs: unified docs architecture — hub + hanzo-docs/<team> submodule pattern (#29)

* docs: redirect /docs/{startups,deploy} → real pages (QA 404 fix)

getting-started references /docs/startups + /docs/deploy but those slugs 404
(the mdx pages didn't survive an earlier merge). Redirect to the pages that
carry the content: getting-started ($5/startup on-ramp) + services/paas (deploy).

* docs: unified docs architecture — hub + hanzo-docs/<team> submodule pattern

Ratify docs.hanzo.ai as the single unified-build hub and define the one way
content joins it, by kind:
- authored  -> content/docs/<section> (in-hub or hanzo-docs/<team> submodule)
- generated -> openapi + ZAP SDK generators, gitignored, never hand-written
- ported    -> sync-project-docs mirror + upstream attribution

Changes:
- ADR as a rendered docs page (content/docs/contributing/docs-architecture.mdx)
  + nav entry; LLM.md canonical model + dedup/rollout debt.
- Wire hanzo-docs/studio-docs as the reference exemplar: git submodule at
  content/docs/studio/ (was a single tracked file). deploy-docs.yml checkout
  now fetches submodules recursively.
- Security: cf-pages-deploy.mjs no longer hard-codes a Cloudflare API token;
  it is required from env/KMS and fails fast (token must be rotated).

Standalone-vs-hub criteria, API-ref-from-openapi, and SDK-slot wiring are all
documented in the ADR. Aggregation proven: 38 API-ref pages generated from 36
openapi specs; studio section resolves from the submodule on disk.

---------

Co-authored-by: hanzo-dev <dev@hanzo.ai>
This commit is contained in:
z
2026-07-08 06:51:38 -07:00
committed by hanzo-dev
co-authored by hanzo-dev
parent cbf29a2672
commit 809d2911e0
9 changed files with 212 additions and 247 deletions
+6
View File
@@ -18,6 +18,12 @@ jobs:
deployments: write
steps:
- uses: actions/checkout@v4
with:
# Team-owned doc sections (hanzo-docs/<team>) are mounted as git
# submodules under apps/docs/content/docs/. Fetch them so the single
# unified build compiles every section. Public HTTPS submodule URLs
# need no extra credentials.
submodules: recursive
- uses: pnpm/action-setup@v4
+3
View File
@@ -0,0 +1,3 @@
[submodule "apps/docs/content/docs/studio"]
path = apps/docs/content/docs/studio
url = https://github.com/hanzo-docs/studio-docs.git
+39
View File
@@ -2,6 +2,45 @@
Fork of [Hanzo Docs](https://github.com/hanzoai/docs) with all packages renamed to `@hanzo/docs-*` namespace.
## Canonical model — the one way to do docs
Full ADR: `apps/docs/content/docs/contributing/docs-architecture.mdx` (rendered
at docs.hanzo.ai/docs/contributing/docs-architecture). Summary:
- **docs.hanzo.ai is the hub.** ONE Fumadocs build (`apps/docs`) → ONE CF Pages
project `hanzo-docs`. The federated per-section deploy is **retired** (stale
origins 530'd). Do not reintroduce per-section builds.
- **One canonical home per doc.** Content enters the single build via exactly one
of three orthogonal mechanisms, chosen by *kind*:
1. **Authored** (first-party prose) → MDX under `content/docs/<section>/`.
Small/core sections live in-repo; team-owned sections live in a
`hanzo-docs/<team>` content repo mounted as a **git submodule** at
`content/docs/<team>/`. Exemplar: `hanzo-docs/studio-docs``content/docs/studio/`.
2. **Generated** (never hand-written, `.gitignore`d) → API reference from
`hanzoai/openapi` via `scripts/sync-openapi.sh` + `scripts/gen-openapi-pages.ts`
(source-derived: add a service spec, it appears next build); SDK reference
from the ZAP SDK generator into `content/docs/sdks/<lang>/`.
3. **Ported** (upstream OSS, mirrored with attribution, `.gitignore`d) →
`scripts/sync-project-docs.ts` into `content/docs/projects/<upstream>/`.
Port, don't re-author. Carry upstream LICENSE + NOTICE. GPL stays GPL.
- **Repos:** `hanzo-docs/docs` = framework + hub (canonical; `hanzoai/docs`
redirects here). `hanzo-docs/<team>` = authored content only, NO framework.
`hanzoai/openapi` = the API source of truth. All docs live in the `hanzo-docs`
org: the hub at `hanzo-docs/docs`, each team's content at `hanzo-docs/<team>`.
- **Standalone vs hub:** default is a hub section. Standalone deploy only if ALL
of: ≈150+ pages or fast OSS-upstream churn, independent versioning, direct
audience. Standalone runs its own copy of this framework; the hub links out,
never copies.
- **Serving:** CF Pages `hanzo-docs` (token from KMS, never hard-coded) or
`ghcr.io/hanzoai/docs` behind hanzoai/ingress. No nginx/caddy.
**Known dedup debt (rollout, not done):** the `apps/*-docs` legacy apps
(base-docs, bootnode-docs, bot-docs, cloud, dev-docs, dns-docs, flow, gui-docs,
insights-docs, platform, pulsar-docs, spec, tasks-docs, team, visor, zen-docs,
zt-docs) predate the unified `content/docs/` model — migrate their content into
`content/docs/<section>/` (or a `hanzo-docs/<team>` submodule) then archive the
app.
## Branch Convention
- **`main`** — Production branch. CF Pages deploys docs.hanzo.ai from here. All Hanzo work lands here.
@@ -0,0 +1,142 @@
---
title: Docs Architecture (ADR)
description: The one and only way Hanzo documentation is sourced, built, and joined into docs.hanzo.ai. Hub-and-spoke over a single unified build.
icon: Network
---
import { Callout } from '@hanzo/docs-ui/components/callout';
# Docs Architecture
**Status:** Accepted · **Scope:** all Hanzo, Lux, Zoo, Zen documentation · **Supersedes:** federated per-section deploys (retired)
This is the definitive model for how Hanzo docs are sourced, built, and served.
One hub, one build, one canonical home per document. If a rule here conflicts
with anything you find in a repo, this wins.
## The decision
<Callout type="info">
**docs.hanzo.ai is the hub.** It is a single [Fumadocs](https://fumadocs.dev)
build (`apps/docs` in `hanzoai/docs`) compiled in one pass and deployed to a
single Cloudflare Pages project (`hanzo-docs`). Every doc renders here unless it
qualifies for a standalone site, in which case the hub links out — it never
copies.
</Callout>
Three things follow from "one and only one way":
1. **One build.** All content — core guides, every service, every ported OSS
project — compiles in the single `apps/docs` pass. The previous federated
layout (one Pages project per section, stitched by an edge worker) is
**retired**: stale `*.pages.dev` origins 530'd whole sections. Do not
reintroduce per-section deploys.
2. **One canonical home per doc.** Every page has exactly one source of truth.
No page is authored in two places. Duplication is a bug.
3. **One join mechanism per source *kind*.** How content enters the build is
determined by *what kind of content it is* — authored, generated, or ported —
not by who owns it. Three kinds, three orthogonal mechanisms (below).
## Repositories
| Repo | Holds | Never holds |
|------|-------|-------------|
| **`hanzo-docs/docs`** | The framework (`@hanzo/docs-*` packages) **and** the hub app (`apps/docs`). Canonical — `hanzoai/docs` redirects here. | Team-owned content that wants its own release cadence. |
| **`hanzo-docs/<team>`** | *Authored* content only — MDX + `meta.json` + `LICENSE`/`NOTICE`. No framework, no build config. | Any Next.js/Fumadocs wiring. It is content, not a site. |
| **`hanzoai/openapi`** | The OpenAPI 3.1 spec for every service. Source of truth for **all** API reference. | Hand-written prose reference. |
All docs live in the **`hanzo-docs`** org: the hub + framework at
`hanzo-docs/docs`, each team's content at `hanzo-docs/<team>`. The API source of
truth stays with the code, at `hanzoai/openapi`. A `hanzo-docs/<team>` repo (any
but `docs`) that carries a framework `package.json` has drifted — fix it.
## The three content mechanisms
Content enters the unified build through exactly one of three paths, chosen by
kind. They are orthogonal: a change to one never touches another.
### 1. Authored — first-party content we write
Prose guides, concepts, tutorials. Lives as MDX under `content/docs/`.
- **Small / core sections** live directly in `hanzoai/docs` at
`apps/docs/content/docs/<section>/`. No repo sprawl for a handful of pages.
- **Team-owned sections** live in their own `hanzo-docs/<team>` repo and are
mounted into the hub as a **git submodule** at
`apps/docs/content/docs/<team>/`. The hub pins a commit; the team owns the
content. The build globs `content/docs/**/*.mdx` and neither knows nor cares
that a directory is a submodule — the join is pure git, zero build code.
Choose a submodule when a team needs its own release cadence, external
contributors, or a large surface. Otherwise keep it in-hub. `hanzo-docs/studio-docs`
mounted at `content/docs/studio/` is the reference exemplar.
### 2. Generated — never hand-written
API reference and SDK reference are **generated** from a machine-readable source
and are `.gitignore`d build artifacts. Editing a generated page is forbidden;
edit the source.
- **API reference** ← `hanzoai/openapi`. `scripts/sync-openapi.sh` copies every
`<service>/openapi.yaml` into `openapi-specs/` (source-derived — no hardcoded
service list; adding a service to `hanzoai/openapi` makes it appear on the
next build). `scripts/gen-openapi-pages.ts` renders one static MDX page per
spec into `content/docs/openapi/<service>.mdx`, cross-linked to the matching
prose guide. **38 reference pages from 36 specs today, zero hand-authored.**
- **SDK reference** ← the ZAP SDK generator (per language). Generated MDX lands
in `content/docs/sdks/<lang>/`. The slot exists (`python`, `typescript`, `go`,
`rust`, `c`); the generator writes into it. Do not hand-author SDK method
tables — regenerate them.
### 3. Ported — upstream OSS docs, mirrored with attribution
For forks (Studio←ComfyUI, Engine←mistral.rs, Flow←Langflow, …) we **port, we do
not re-author**. `scripts/sync-project-docs.ts` mirrors upstream docs into
`content/docs/projects/<upstream>/` (source-derived from the GitHub orgs;
`.gitignore`d). Foreign JSX (Mintlify/Docusaurus/GitBook components) is
neutralised so a single build stays safe. Every ported tree carries the
upstream `LICENSE` and a `NOTICE` crediting the source. Respect the upstream
license — GPL content stays GPL; attribution is not optional.
<Callout type="warn">
Authored vs ported is the sharpest line. `hanzo-docs/studio-docs` holds
*our* Studio guides (first-party). The wholesale ComfyUI manual is *ported*
into `content/docs/projects/comfyui/`. They never mix.
</Callout>
## Hub section vs standalone site
Default is a hub section. A product earns a **standalone** deploy only when **all
three** hold:
1. **Large** — the surface is big enough that co-building it in the hub hurts
build time (≈150+ pages), **or** it tracks a fast-moving OSS upstream.
2. **Independently versioned** — it ships docs on its own cadence, decoupled from
the platform.
3. **Direct audience** — users land on it directly, not via the platform hub.
When a product goes standalone it runs its **own copy of this same framework**
(`@hanzo/create-docs`), and the hub shows a card that **links out**. The content
still has exactly one home; it is **not** also aggregated into the hub. No
duplication, ever. Everything else is a hub section.
## Serving
- **Primary:** static export → Cloudflare Pages project `hanzo-docs`
(`scripts/cf-pages-deploy.mjs`). The deploy token comes from KMS
(`kms.hanzo.ai`) at run time — never hard-coded.
- **In-cluster option:** `ghcr.io/hanzoai/docs` behind **hanzoai/ingress**
(`k8s/docs-landing.yml`). No nginx, no caddy — the Hanzo ingress + static
plugin only.
White-label by hostname via `@hanzo/docs-org` presets (Hanzo / Lux / Zoo / Zen).
One framework, four brands, resolved at request time.
## Adding docs — the decision in one paragraph
Writing a guide for an existing product? Add MDX under its
`content/docs/<section>/` (or its `hanzo-docs/<team>` submodule). Documenting an
API? Don't — update `hanzoai/openapi` and it regenerates. Documenting an SDK?
Don't — the ZAP generator emits it. Bringing a fork's upstream manual? Add the
repo to `sync-project-docs` config; it mirrors with attribution. Nothing else is
a supported way to add docs.
@@ -0,0 +1,6 @@
{
"title": "Contributing",
"description": "How Hanzo docs are built, sourced, and joined into docs.hanzo.ai",
"icon": "GitPullRequest",
"pages": ["docs-architecture"]
}
+3 -1
View File
@@ -86,6 +86,8 @@
"---Research---",
"research",
"---Open Source---",
"projects"
"projects",
"---Contributing---",
"contributing"
]
}
-243
View File
@@ -1,243 +0,0 @@
---
title: Hanzo Studio
description: Visual AI engine for building image, video, 3D, and audio workflows. Node-based editor with GPU scaling, multi-tenant isolation, and enterprise billing.
index: true
---
import { Card, Cards } from '@hanzo/docs-base-ui/components/card';
# Hanzo Studio
Visual AI engine for production workflows. Build **image generation**, **video synthesis**, **3D rendering**, and **audio processing** pipelines with a drag-and-drop node editor -- then scale to GPU when you need real throughput.
Available at [studio.hanzo.ai](https://studio.hanzo.ai) or self-hosted via Docker. Built on [ComfyUI](https://github.com/comfyanonymous/ComfyUI) with full node compatibility.
```bash title="Quick Start"
docker pull ghcr.io/hanzoai/studio:latest
docker run -p 8188:8188 ghcr.io/hanzoai/studio:latest \
python main.py --listen 0.0.0.0 --cpu
# Open http://localhost:8188
```
## Why Hanzo Studio?
- **Visual Node Editor** -- Wire together models, preprocessors, and outputs in a graph-based canvas. Every node is composable.
- **GPU on Demand** -- Start on CPU for prototyping. Switch to T4, A100, or H100 with one API call when you're ready for production.
- **Distributed Workers** -- Route GPU-heavy prompts to dedicated worker machines. The coordinator handles queuing, routing, and result aggregation.
- **Multi-Tenant Isolation** -- Each organization gets isolated storage, compute profiles, and usage tracking. IAM authentication via [hanzo.id](https://hanzo.id).
- **Usage Billing** -- Metered billing per prompt execution via Hanzo Commerce. Per-second pricing, minimum 1 credit per run.
- **Full ComfyUI Compatibility** -- Every custom node, model, and workflow from the ComfyUI ecosystem works out of the box. No vendor lock-in.
## Compute Profiles
Each organization gets a compute profile that determines how prompts are executed. Switch profiles at any time via the API.
| Profile | Device | GPU | VRAM | Best For |
|---------|--------|-----|------|----------|
| `cpu` | CPU | -- | -- | Prototyping, lightweight workflows |
| `gpu-basic` | CUDA | T4 | 16 GB | Image generation, standard diffusion models |
| `gpu-pro` | CUDA | A100 | 80 GB | Large models, video generation, 3D |
| `gpu-max` | CUDA | H100 | 80 GB | Maximum throughput, batch processing |
| `custom` | CUDA | Any | Any | Bring your own instance type |
### Switch to GPU
```bash
curl -X PUT https://studio.hanzo.ai/api/compute/config \
-H "Authorization: Bearer $HANZO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"active_profile": "gpu-basic",
"gpu_type": "t4",
"auto_provision": true
}'
```
When `auto_provision` is enabled, Studio automatically launches a GPU VM through [Visor](https://visor.hanzo.ai) on first GPU prompt. The worker registers itself within 60 seconds and begins accepting work.
```bash title="Check Current Config"
curl https://studio.hanzo.ai/api/compute/config \
-H "Authorization: Bearer $HANZO_TOKEN"
```
## GPU Workers
Studio supports distributed execution via **worker mode**. A coordinator (the main Studio instance) routes prompts to GPU workers based on the active compute profile.
### How It Works
```
Client ──prompt──▸ Coordinator (studio.hanzo.ai)
├── CPU prompt ──▸ local execution
├── GPU prompt ──▸ Worker 1 (T4, 16GB)
└── GPU prompt ──▸ Worker 2 (A100, 80GB)
```
1. Client submits a prompt with optional `device_preference`
2. Coordinator checks the org's compute profile
3. If GPU requested, the prompt is forwarded to an available GPU worker via HTTP
4. Worker executes locally, returns the result
5. Coordinator forwards the result back to the client WebSocket
### Launch a GPU Worker
```bash
curl -X POST https://studio.hanzo.ai/api/compute/provision \
-H "Authorization: Bearer $HANZO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"gpu_type": "t4",
"worker_id": "my-gpu-worker"
}'
```
Workers auto-register with the coordinator via heartbeats every 30 seconds. Stale workers (no heartbeat for 90s) are removed from the pool.
### Self-Hosted Workers
Run a Studio instance as a headless GPU worker that registers with your coordinator:
```bash
docker run --gpus all -p 8188:8188 \
ghcr.io/hanzoai/studio:latest \
python main.py \
--listen 0.0.0.0 \
--worker-mode \
--coordinator-url http://your-coordinator:8188 \
--worker-id gpu-worker-1
```
### List Workers
```bash
curl https://studio.hanzo.ai/api/compute/workers \
-H "Authorization: Bearer $HANZO_TOKEN"
```
Returns status, device info, VRAM, and last heartbeat for each registered worker.
## Prompt Routing
Control where prompts execute with the `device_preference` field:
```bash
curl -X POST https://studio.hanzo.ai/api/prompt \
-H "Authorization: Bearer $HANZO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"prompt": { ... },
"device_preference": "gpu"
}'
```
| Preference | Behavior |
|------------|----------|
| `auto` (default) | GPU if enabled in profile and worker available, else CPU |
| `cpu` | Always execute locally on CPU |
| `gpu` | Route to GPU worker. Returns 503 if none available |
When `auto_provision` is enabled and no GPU worker is available, Studio returns `202 Accepted` and begins provisioning. Poll the workers endpoint for status.
## API Reference
| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/prompt` | Submit a workflow prompt |
| `GET` | `/api/queue` | Queue status and pending jobs |
| `GET` | `/api/jobs` | List jobs with filtering |
| `GET` | `/api/system_stats` | Device info, memory, VRAM |
| `GET` | `/api/compute/config` | Get org compute profile |
| `PUT` | `/api/compute/config` | Update compute profile |
| `GET` | `/api/compute/workers` | List registered workers |
| `POST` | `/api/compute/workers/register` | Worker heartbeat (internal) |
| `POST` | `/api/compute/provision` | Launch GPU VM via Visor |
| `DELETE` | `/api/compute/provision` | Terminate GPU VM |
## Self-Hosted Deployment
### CPU Only
```bash
docker run -d --name studio \
-p 8188:8188 \
-v studio-data:/app/user \
ghcr.io/hanzoai/studio:latest \
python main.py --listen 0.0.0.0 --cpu
```
### GPU (NVIDIA)
```bash
docker run -d --name studio --gpus all \
-p 8188:8188 \
-v studio-data:/app/user \
ghcr.io/hanzoai/studio:latest \
python main.py --listen 0.0.0.0
```
### With IAM Auth + Billing
```yaml title="compose.yml"
services:
studio:
image: ghcr.io/hanzoai/studio:latest
ports:
- "8188:8188"
environment:
STUDIO_ENABLE_IAM_AUTH: "true"
STUDIO_IAM_URL: "https://hanzo.id"
STUDIO_NO_LOCALHOST_BYPASS: "true"
STUDIO_MULTI_TENANT: "true"
STUDIO_ORG_ID: "my-org"
STUDIO_ENABLE_BILLING: "true"
STUDIO_COMMERCE_URL: "http://commerce.hanzo.svc:8001"
STUDIO_COMMERCE_TOKEN: "${COMMERCE_TOKEN}"
volumes:
- studio-data:/app/user
command: ["python", "main.py", "--listen", "0.0.0.0", "--cpu"]
volumes:
studio-data:
```
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `STUDIO_ENABLE_IAM_AUTH` | `false` | Enable IAM authentication via hanzo.id |
| `STUDIO_IAM_URL` | `https://hanzo.id` | IAM server URL |
| `STUDIO_NO_LOCALHOST_BYPASS` | `false` | Require auth even for localhost |
| `STUDIO_MULTI_TENANT` | `false` | Per-org storage isolation |
| `STUDIO_ORG_ID` | -- | Default organization ID |
| `STUDIO_ENABLE_BILLING` | `false` | Enable Commerce usage billing |
| `STUDIO_COMMERCE_URL` | `http://commerce.hanzo.svc:8001` | Commerce API endpoint |
| `STUDIO_RATE_LIMIT_RPM` | `60` | Max prompts per minute per org |
| `STUDIO_ENABLE_METRICS` | `false` | Enable Prometheus `/metrics` endpoint |
| `STUDIO_WORKER_MODE` | `false` | Run as headless GPU worker |
| `COORDINATOR_URL` | -- | Coordinator URL (worker mode only) |
| `WORKER_ID` | -- | Unique worker identifier |
| `VISOR_URL` | `https://visor.hanzo.ai` | Visor API for GPU provisioning |
## Get Started
1. Sign in at [studio.hanzo.ai](https://studio.hanzo.ai) with your Hanzo account
2. Build a workflow in the visual node editor
3. Submit prompts -- runs on CPU by default
4. When you need GPU, call `PUT /api/compute/config` to switch profiles
5. Studio provisions GPU workers automatically and routes your prompts
<Cards>
<Card title="Hanzo Chat" href="/docs/chat">
AI chat with Zen models, MCP and ZAP tools
</Card>
<Card title="LLM Gateway" href="/docs/llm">
Unified API proxy for 200+ language models
</Card>
<Card title="MCP Tools" href="/docs/mcp">
Model Context Protocol tool integrations
</Card>
<Card title="API Reference" href="/docs/openapi">
Full OpenAPI specification for all services
</Card>
</Cards>
+12 -3
View File
@@ -7,9 +7,18 @@ import { createHash } from 'crypto';
import { readdir, readFile, stat } from 'fs/promises';
import { join, relative } from 'path';
const ACCOUNT_ID = '94a3e3f299092abb1feda2a7481ea845';
const PROJECT_NAME = 'hanzo-docs';
const API_TOKEN = process.env.CLOUDFLARE_API_TOKEN || 'bBEtGaPXpGSmvC5w2WbpE2V7zx08VDWspCS-E30g';
const ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID || '94a3e3f299092abb1feda2a7481ea845';
const PROJECT_NAME = process.env.CF_PAGES_PROJECT || 'hanzo-docs';
// Never hard-code the token. CI injects CLOUDFLARE_API_TOKEN from KMS
// (kms.hanzo.ai → the docs deploy service account). Fail fast if absent.
const API_TOKEN = process.env.CLOUDFLARE_API_TOKEN;
if (!API_TOKEN) {
console.error(
'CLOUDFLARE_API_TOKEN is required. It is provisioned from KMS (kms.hanzo.ai) in CI; ' +
'for a local deploy export it from your KMS session. Refusing to run without it.',
);
process.exit(1);
}
const BASE_URL = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/pages/projects/${PROJECT_NAME}`;
const BATCH_SIZE = 50; // files per upload batch
const MAX_RETRIES = 3;