Comprehensive guide for building and managing zo.space routes (pages and APIs). Use this skill whenever creating, editing, or debugging zo.space routes. Covers architecture, available packages, patterns, limitations, and gotchas.
Scanned 9/10/2026
Install to Claude Code
npx -y skills add luokai0/ai-agent-skills-by-luo-kai --skill zo-space --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Zo Space?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/luokai0-zo-space)More formats (shields.io, HTML) on the badges page.
---
name: zo-space
description: >
Comprehensive guide for building and managing zo.space routes (pages and APIs).
Use this skill whenever creating, editing, or debugging zo.space routes.
Covers architecture, available packages, patterns, limitations, and gotchas.
compatibility: Created for Zo Computer
metadata:
author: skeletorjs
version: "1.0"
category: Community
---
# zo.space Development Guide
Read this skill before creating or editing any zo.space route.
For deeper reference on specific topics, read the files in `references/`.
## Architecture Overview
zo.space is a per-user web presence at `{handle}.zo.space`. It runs:
- **Runtime**: Bun 1.2.x
- **Server framework**: Hono (all HTTP routing)
- **Frontend**: React 19 + Vite 7 (SPA with client-side routing via react-router-dom)
- **Styling**: Tailwind CSS 4 with `@tailwindcss/vite` plugin + tw-animate-css
- **Icons**: `lucide-react` and `@tabler/icons-react` are both available
Routes are NOT files in the user's workspace. They exist only in the space system at `/__substrate/space/routes/`. Manage them exclusively through the space tools.
## Tools
| Tool | Purpose |
|------|---------|
| `list_space_routes()` | List all routes (paths, types, visibility) |
| `get_space_route(path)` | Get a route's full source code |
| `update_space_route(path, route_type, code, public)` | Create or update a route |
| `delete_space_route(path)` | Delete a route |
| `list_space_assets()` | List uploaded static assets |
| `update_space_asset(source_file, asset_path)` | Upload a workspace file as a static asset |
| `delete_space_asset(asset_path)` | Delete an asset |
| `get_space_errors()` | Check for build/runtime errors |
## Route Types
### Page Routes (`route_type="page"`)
React/TSX components rendered client-side. Default export must be a React component.
```tsx
import { useState } from "react";
import { Globe } from "lucide-react";
export default function MyPage() {
const [count, setCount] = useState(0);
return (
<div className="min-h-screen bg-background text-foreground p-8">
<Globe className="w-8 h-8 text-primary" />
<h1 className="text-3xl font-bold">Hello</h1>
<button onClick={() => setCount(c => c + 1)} className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-lg">
Clicked {count} times
</button>
</div>
);
}
```
Visibility:
- `public=false` (default): requires auth (only the user can see it)
- `public=true`: anyone can view
Pages are compiled by Vite into the SPA bundle. They run in BrowserRouter, wrapped in ErrorBoundary and Suspense.
### API Routes (`route_type="api"`)
Hono handler functions. Default export must be a function that takes a Hono Context and returns a Response.
```typescript
import type { Context } from "hono";
export default async (c: Context) => {
return c.json({ message: "Hello from the API" });
};
```
API routes are **always public** regardless of the `public` parameter.
The handler receives ALL HTTP methods through `app.all()`. Check `c.req.method` to handle different methods:
```typescript
import type { Context } from "hono";
export default async (c: Context) => {
if (c.req.method === "GET") {
return c.json({ items: [] });
}
if (c.req.method === "POST") {
const body = await c.req.json();
return c.json({ created: true });
}
return c.json({ error: "Method not allowed" }, 405);
};
```
## How the Build Pipeline Works
When you call `update_space_route()`:
1. The route code is written to `/__substrate/space/routes/api/{name}.ts` or `/__substrate/space/routes/pages/{name}.tsx`
2. An index file is regenerated that imports all routes
3. For pages: `vite build` runs, compiling all page routes into the SPA bundle at `/__substrate/space/dist/`
4. For APIs: routes are dynamically imported by the Hono server at startup
5. The `zo-space` supervisor service is restarted
This means:
- **Page route updates trigger a full Vite rebuild.** This takes a few seconds.
- **All page routes are compiled together.** A syntax error in one page can break all pages.
- **API routes are loaded via dynamic import.** An error in one API route won't break others.
Path-to-filename mapping:
- `/` becomes `_home`
- `/hello` becomes `hello`
- `/api/hello` becomes `api-hello`
- `/users/list` becomes `users-list`
Reserved routes that cannot be overwritten:
- `/api/_health` (health check)
- `/api/_error` (client-side error reporting)
## Available Packages
These are pre-installed and available without any extra setup:
### Server-side (API routes):
| Package | Version | Notes |
|---------|---------|-------|
| `hono` | ^4.10.x | Web framework. Import `Context` type from `"hono"` |
| `stripe` | ^17.7.x | Stripe SDK for payment processing |
| `marked` | ^17.0.x | Markdown parser |
| `zod` | ^4.1.x | Schema validation |
API routes also have access to:
- **All Bun APIs**: `Bun.spawn()`, `Bun.file()`, `Bun.write()`, `Bun.serve()`, etc.
- **Node.js built-ins**: `node:fs`, `node:path`, `node:child_process`, etc.
- **`process.env`**: All environment variables including secrets from Settings > Advanced
- **File system**: Full read/write access to `/home/workspace` and the rest of the system
- **Shell commands**: Via `Bun.spawn()` -- can run duckdb, python, etc.
- **Network**: Can make outbound HTTP requests via `fetch()`
- **`ZO_CLIENT_IDENTITY_TOKEN`**: For calling the Zo API (`https://api.zo.computer/zo/ask`)
### Client-side (page routes):
| Package | Use |
|---------|-----|
| `react` / `react-dom` | ^19.2.x |
| `react-router-dom` | ^7.9.x -- client-side routing |
| `lucide-react` | ^0.562.x -- icons |
| `@tabler/icons-react` | ^3.35.x -- icons |
| `recharts` | ^3.6.x -- charts and data viz |
| `@tanstack/react-table` | ^8.21.x -- table component |
| `@dnd-kit/*` | Drag and drop |
| `class-variance-authority` | Variant styling utility |
| `clsx` + `tailwind-merge` | Class name utilities |
| `sonner` | Toast notifications |
| `vaul` | Drawer component |
| `reveal.js` | ^5.2.x -- presentations |
| `next-themes` | Theme switching (dark/light/system) |
| `@tailwindcss/typography` | Prose styling for rendered content |
### Radix UI Primitives (via shadcn/ui pattern):
`avatar`, `checkbox`, `dialog`, `dropdown-menu`, `label`, `select`, `separator`, `slot`, `tabs`, `toggle`, `toggle-group`, `tooltip`
Note: Only `card` and `chart` shadcn components are pre-built in `src/components/ui/`. For other Radix primitives, import directly from `@radix-ui/react-*`.
## Styling
### Tailwind CSS 4
Tailwind 4 uses CSS-first configuration. The theme is defined in `/__substrate/space/src/styles.css` using CSS variables and `@theme inline`.
Key semantic color classes (use these, not raw colors):
- `bg-background` / `text-foreground` -- main page colors
- `bg-card` / `text-card-foreground` -- card surfaces
- `bg-primary` / `text-primary-foreground` -- primary actions
- `bg-secondary` / `text-secondary-foreground` -- secondary elements
- `bg-muted` / `text-muted-foreground` -- subdued content
- `bg-accent` / `text-accent-foreground` -- accents
- `bg-destructive` -- destructive/error actions
- `border-border` -- standard border color
- `bg-input` -- input backgrounds
- `ring-ring` -- focus rings
Chart colors: `text-chart-1` through `text-chart-5`
Dark mode is class-based (`.dark` on `<html>`). The ThemeProvider handles this automatically. All semantic color tokens have dark mode variants defined.
Border radius uses `--radius` variable: `rounded-sm`, `rounded-md`, `rounded-lg`, `rounded-xl`.
### tw-animate-css
Animation utilities are available. The `@import "tw-animate-css"` is already included.
### @tailwindcss/typography
The `prose` class is available for rendering rich text/markdown content:
```tsx
<div className="prose dark:prose-invert">
{/* rendered markdown here */}
</div>
```
## Patterns and Best Practices
### Fetching data from API routes in pages
Pages can call API routes on the same domain using relative URLs:
```tsx
const [data, setData] = useState(null);
useEffect(() => {
fetch("/api/my-endpoint")
.then(r => r.json())
.then(setData);
}, []);
```
### Querying DuckDB from API routes
```typescript
async function queryDb(sql: string): Promise<any[]> {
const proc = Bun.spawn(["duckdb", "/home/workspace/Data/tasks.db", "-json", "-c", sql], {
stdout: "pipe",
stderr: "pipe",
});
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
await proc.exited;
if (proc.exitCode !== 0) throw new Error(stderr);
return JSON.parse(stdout || "[]");
}
```
### Calling the Zo API from API routes
```typescript
const token = process.env.ZO_CLIENT_IDENTITY_TOKEN;
const response = await fetch("https://api.zo.computer/zo/ask", {
method: "POST",
headers: {
"authorization": token,
"content-type": "application/json",
},
body: JSON.stringify({ input: "Your prompt here" }),
});
const result = await response.json();
```
### Using Stripe webhooks
The `stripe` package is pre-installed. Use `process.env.STRIPE_SECRET_KEY` and `process.env.STRIPE_WEBHOOK_SECRET` from Settings > Advanced secrets.
### Reading workspace files from API routes
```typescript
const file = Bun.file("/home/workspace/path/to/file.json");
const content = await file.json();
```
### Using static assets
1. Upload from workspace: `update_space_asset("/home/workspace/image.png", "/images/hero.png")`
2. Reference in pages: `<img src="/images/hero.png" />`
Assets are served from `/__substrate/space/assets/` and are always public.
## Gotchas and Limitations
### Build failures break all pages
A syntax error in any page route will cause the Vite build to fail, breaking ALL pages. API routes are more resilient since they're imported individually. Always check `get_space_errors()` after updating page routes.
### No hot reload
Route updates require a full rebuild + server restart. The server is managed by supervisord as `zo-space`.
### API routes get ALL methods
Handlers are registered with `app.all()`, not method-specific routes. You must check `c.req.method` yourself if you want to restrict methods.
### No shared state between API routes
Each API route file is a standalone module. There's no shared in-memory state between routes (except through file system, databases, or env vars). The server does maintain module-level state within a single route between requests.
### Server crash loop
The zo-space server can enter a crash loop if it fails to start (e.g., port conflict, bad route code). Check `/dev/shm/zo-space.log` and `/dev/shm/zo-space_err.log` for diagnostics. The supervisor will keep trying to restart it.
### Page routes are single-file
Each page route is one TSX file. You can't split a page into multiple component files. Put all components inline in the same file, or fetch shared data from API routes.
### No custom npm packages in routes
You can only use packages already installed in `/__substrate/space/node_modules/`. You cannot add new npm dependencies via route code. Work with what's available or implement logic inline.
### `import.meta.env.VITE_HANDLE` in pages
To get the user's handle in page routes, use `import.meta.env.VITE_HANDLE`.
### `process.env` only works server-side
Environment variables via `process.env` are only available in API routes, not in page routes. For pages, use `import.meta.env.VITE_*` variables.
### No path aliases in routes
The `@/` alias resolves to `/__substrate/space/src/`. Routes live in `/__substrate/space/routes/`. Importing from `@/components/ui/card` works but it's reaching into the SPA source. Use it for the pre-built shadcn components, but don't rely on it for custom code.
### Error reporting
- Build errors: written to `/__substrate/space/.errors/_build.json`
- Runtime errors: written to `/__substrate/space/.errors/{route}.json`
- Client errors: posted to `/api/_error` by the ErrorBoundary
- All retrievable via `get_space_errors()`
### Proxy logs
Request logs are at `/dev/shm/zosite-3099-proxy.log`. Server logs at `/dev/shm/zo-space.log`.
## Debugging Workflow
1. `get_space_errors()` -- check for build or runtime errors
2. Read `/dev/shm/zo-space.log` for server output
3. Read `/dev/shm/zo-space_err.log` for crash/restart info
4. Read `/dev/shm/zosite-3099-proxy.log` for HTTP request logs
5. If server is in crash loop, check error logs and fix the offending route
6. After fixing, the supervisor will automatically restart the server
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!