# Toes User Guide Toes is a personal web appliance that runs multiple web apps on your home network. Plug it in, turn it on, and forget about the cloud. ## Table of Contents - [Quick Start](#quick-start) - [Creating an App](#creating-an-app) - [App Templates](#app-templates) - [App Structure](#app-structure) - [The Bare Minimum](#the-bare-minimum) - [Using Hype](#using-hype) - [Using Forge](#using-forge) - [Creating a Tool](#creating-a-tool) - [What's a Tool?](#whats-a-tool) - [Tool Setup](#tool-setup) - [Theme Tokens](#theme-tokens) - [Accessing App Data](#accessing-app-data) - [CLI Reference](#cli-reference) - [App Management](#app-management) - [Lifecycle](#lifecycle) - [Syncing Code](#syncing-code) - [Environment Variables](#environment-variables) - [Versioning](#versioning) - [Cron Jobs](#cron-jobs-1) - [Metrics](#metrics) - [Sharing](#sharing) - [Configuration](#configuration) - [Environment Variables](#environment-variables-1) - [Health Checks](#health-checks) - [App Lifecycle](#app-lifecycle) - [Cron Jobs](#cron-jobs) - [Data Persistence](#data-persistence) --- ## Quick Start ```bash # Install the CLI curl -fsSL http://toes.local/install | bash # Create a new app toes new my-app # Enter the directory, install deps, and develop locally cd my-app bun install bun dev # Push to the server toes push # Open in browser toes open ``` Your app is now running at `http://my-app.toes.local`. --- ## Creating an App ### App Templates Toes ships with three templates. Pick one when creating an app: ```bash toes new my-app # SSR (default) toes new my-app --bare # Minimal toes new my-app --spa # Single-page app ``` **SSR** — Server-side rendered with a pages directory. Best for most apps. Uses Hype's built-in layout and page routing. **Bare** — Just an `index.tsx` with a single route. Good when you want to start from scratch. **SPA** — Client-side rendering with `hono/jsx/dom`. Hype serves the HTML shell and static files; the browser handles routing and rendering. ### App Structure A generated SSR app looks like this: ``` my-app/ .npmrc # Points to the private registry .toesignore # Files to exclude from sync (like .gitignore) package.json # Must have scripts.toes tsconfig.json # TypeScript config index.tsx # Entry point (re-exports from src/server) src/ server/ index.tsx # Hype app with routes pages/ index.tsx # Page components ``` ### The Bare Minimum Every app needs three things: 1. **`package.json`** with a `scripts.toes` entry 2. **`index.tsx`** that exports `app.defaults` 3. **A `GET /ok` route** that returns 200 (health check) **package.json:** ```json { "name": "my-app", "module": "index.tsx", "type": "module", "private": true, "scripts": { "toes": "bun run --watch index.tsx" }, "toes": { "icon": "🎨" }, "dependencies": { "@because/hype": "*", "@because/forge": "*" } } ``` The `scripts.toes` field is how Toes discovers your app. The `toes.icon` field sets the emoji shown in the dashboard. **.npmrc:** ``` registry=https://npm.nose.space ``` Required for installing `@because/*` packages. **index.tsx:** ```tsx import { Hype } from '@because/hype' const app = new Hype() app.get('/', c => c.html(

Hello World

)) app.get('/ok', c => c.text('ok')) export default app.defaults ``` That's it. Push to the server and it runs. ### Using Hype Hype wraps [Hono](https://hono.dev). Everything you know from Hono works here. Hype adds a few extras: **Basic routing:** ```tsx import { Hype } from '@because/hype' const app = new Hype() app.get('/', c => c.html(

Home

)) app.get('/about', c => c.html(

About

)) app.post('/api/items', async c => { const body = await c.req.json() return c.json({ ok: true }) }) app.get('/ok', c => c.text('ok')) export default app.defaults ``` **Sub-routers:** ```tsx const api = Hype.router() api.get('/items', c => c.json([])) api.post('/items', async c => { const body = await c.req.json() return c.json({ ok: true }) }) app.route('/api', api) // mounts at /api/items ``` **Server-Sent Events:** ```tsx app.sse('/stream', (send, c) => { send({ hello: 'world' }) const interval = setInterval(() => send({ time: Date.now() }), 1000) return () => clearInterval(interval) // cleanup on disconnect }) ``` **Constructor options:** ```tsx const app = new Hype({ layout: true, // Wraps pages in an HTML layout (default: true) prettyHTML: true, // Pretty-print HTML output (default: true) logging: true, // Log requests to stdout (default: true) }) ``` ### Using Forge Forge is a CSS-in-JS library that creates styled JSX components. Define a component once, use it everywhere. **Basic usage:** ```tsx import { define, stylesToCSS } from '@because/forge' const Box = define('Box', { padding: 20, borderRadius: '6px', backgroundColor: '#f5f5f5', }) // content renders
content
``` Numbers auto-convert to `px` (except `flex`, `opacity`, `zIndex`, `fontWeight`). **Set the HTML element:** ```tsx const Button = define('Button', { base: 'button', padding: '8px 16px' }) const Link = define('Link', { base: 'a', textDecoration: 'none' }) const Input = define('Input', { base: 'input', padding: 8, border: '1px solid #ccc' }) ``` **Pseudo-classes (`states`):** ```tsx const Item = define('Item', { padding: 12, states: { ':hover': { backgroundColor: '#eee' }, ':last-child': { borderBottom: 'none' }, }, }) ``` **Nested selectors:** ```tsx const List = define('List', { selectors: { '& > li:last-child': { borderBottom: 'none' }, }, }) ``` **Variants:** ```tsx const Button = define('Button', { base: 'button', padding: '8px 16px', variants: { variant: { primary: { backgroundColor: '#2563eb', color: 'white' }, danger: { backgroundColor: '#dc2626', color: 'white' }, }, }, }) // ``` **Serving CSS:** Forge generates CSS at runtime. Serve it from a route: ```tsx import { stylesToCSS } from '@because/forge' app.get('/styles.css', c => c.text(stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8' }) ) ``` Then link it in your HTML: ```tsx ``` --- ## Creating a Tool ### What's a Tool? A tool is an app that appears as a tab inside the Toes dashboard instead of in the sidebar. Tools render in an iframe and receive the currently selected app as a `?app=` query parameter. Good for things like a code editor, log viewer, env manager, or cron scheduler. From the server's perspective, a tool is identical to an app — same lifecycle, same health checks, same port allocation. The only differences are in `package.json` and how you render. ### Tool Setup A tool needs three extra things compared to a regular app: 1. Set `"tool": true` in `package.json` 2. Include `` in the HTML body 3. Prepend `baseStyles` to CSS output **package.json:** ```json { "name": "my-tool", "module": "index.tsx", "type": "module", "private": true, "scripts": { "toes": "bun run --watch index.tsx" }, "toes": { "icon": "🔧", "tool": true }, "dependencies": { "@because/forge": "*", "@because/hype": "*", "@because/toes": "*" } } ``` Set `"tool"` to `true` for a tab labeled with the app name, or to a string for a custom label (e.g., `"tool": ".env"`). **index.tsx:** ```tsx import { Hype } from '@because/hype' import { define, stylesToCSS } from '@because/forge' import { baseStyles, ToolScript, theme } from '@because/toes/tools' import type { Child } from 'hono/jsx' const app = new Hype({ prettyHTML: false }) const Container = define('Container', { fontFamily: theme('fonts-sans'), padding: '20px', paddingTop: 0, maxWidth: '800px', margin: '0 auto', color: theme('colors-text'), }) function Layout({ title, children }: { title: string; children: Child }) { return ( {title} {children} ) } app.get('/ok', c => c.text('ok')) app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8' }) ) app.get('/', async c => { const appName = c.req.query('app') if (!appName) { return c.html(

No app selected

) } return c.html(

{appName}

Tool content for {appName}

) }) export default app.defaults ``` Key points: - `` handles dark/light mode syncing and iframe height communication with the dashboard. - `baseStyles` sets the body background to match the dashboard theme. - `prettyHTML: false` is recommended for tools since their output is inside an iframe. - The `?app=` query parameter tells you which app the user has selected in the sidebar. ### Theme Tokens Tools should use theme tokens to match the dashboard's look. Import `theme` from `@because/toes/tools`: ```tsx import { theme } from '@because/toes/tools' const Card = define('Card', { color: theme('colors-text'), backgroundColor: theme('colors-bgElement'), border: `1px solid ${theme('colors-border')}`, borderRadius: theme('radius-md'), padding: theme('spacing-lg'), }) ``` Available tokens: | Token | Description | |-------|-------------| | `colors-bg` | Page background | | `colors-bgSubtle` | Subtle background | | `colors-bgElement` | Element background (cards, inputs) | | `colors-bgHover` | Hover background | | `colors-text` | Primary text | | `colors-textMuted` | Secondary text | | `colors-textFaint` | Tertiary/disabled text | | `colors-border` | Borders | | `colors-link` | Link text | | `colors-primary` | Primary action color | | `colors-primaryText` | Text on primary color | | `colors-error` | Error color | | `colors-dangerBorder` | Danger state border | | `colors-dangerText` | Danger text | | `colors-success` | Success color | | `colors-successBg` | Success background | | `colors-statusRunning` | Running indicator | | `colors-statusStopped` | Stopped indicator | | `fonts-sans` | Sans-serif font stack | | `fonts-mono` | Monospace font stack | | `spacing-xs` | 4px | | `spacing-sm` | 8px | | `spacing-md` | 12px | | `spacing-lg` | 16px | | `spacing-xl` | 24px | | `radius-md` | 6px | ### Accessing App Data **Reading app files:** ```tsx import { join } from 'path' const APPS_DIR = process.env.APPS_DIR! app.get('/', c => { const appName = c.req.query('app') if (!appName) return c.html(

No app selected

) const appPath = join(APPS_DIR, appName, 'current') // Read files from appPath... }) ``` Always go through the `current` symlink — never access version directories directly. **Calling the Toes API:** ```tsx const TOES_URL = process.env.TOES_URL! // List all apps const apps = await fetch(`${TOES_URL}/api/apps`).then(r => r.json()) // Get a specific app const app = await fetch(`${TOES_URL}/api/apps/${name}`).then(r => r.json()) ``` **Linking between tools:** ```html Edit in Code ``` Tool URLs go through `/tool/:name` which redirects to the tool's subdomain with query params preserved. **Listening to lifecycle events:** ```tsx import { on } from '@because/toes/tools' const unsub = on('app:start', event => { console.log(`${event.app} started at ${event.time}`) }) // Event types: 'app:start', 'app:stop', 'app:create', 'app:delete', 'app:activate' ``` --- ## CLI Reference The CLI connects to your Toes server over HTTP. By default it connects to `http://toes.local`. Set `TOES_URL` to point elsewhere, or set `DEV=1` to use `http://localhost:3000`. Most commands accept an optional app name. If omitted, the CLI uses the current directory's `package.json` name. ### App Management **`toes list`** — List all apps and their status. ```bash toes list # Show apps and tools toes list --apps # Apps only (exclude tools) toes list --tools # Tools only ``` **`toes new [name]`** — Create a new app from a template. ```bash toes new my-app # SSR template (default) toes new my-app --bare # Minimal template toes new my-app --spa # SPA template ``` Creates the app locally, then pushes it to the server. If run without a name, scaffolds the current directory. **`toes info [name]`** — Show details for an app (state, URL, port, PID, uptime). **`toes get `** — Download an app from the server to your local machine. ```bash toes get my-app # Creates ./my-app/ with all files cd my-app bun install bun dev # Develop locally ``` **`toes open [name]`** — Open a running app in your browser. **`toes rename [name] `** — Rename an app. Requires typing a confirmation. **`toes rm [name]`** — Permanently delete an app from the server. Requires typing a confirmation. ### Lifecycle **`toes start [name]`** — Start a stopped app. **`toes stop [name]`** — Stop a running app. **`toes restart [name]`** — Stop and start an app. **`toes logs [name]`** — View logs for an app. ```bash toes logs my-app # Today's logs toes logs my-app -f # Follow (tail) logs in real-time toes logs my-app -d 2026-01-15 # Logs from a specific date toes logs my-app -s 2d # Logs from the last 2 days toes logs my-app -g error # Filter logs by pattern toes logs my-app -f -g error # Follow and filter ``` Duration formats for `--since`: `1h` (hours), `2d` (days), `1w` (weeks), `1m` (months). ### Syncing Code Toes uses a manifest-based sync protocol. Each file is tracked by SHA-256 hash. The server stores versioned snapshots with timestamps. **`toes push`** — Push local changes to the server. ```bash toes push # Push changes (fails if server changed) toes push --force # Overwrite server changes ``` Creates a new version on the server, uploads changed files, deletes removed files, then activates the new version. The app auto-restarts. **`toes pull`** — Pull changes from the server. ```bash toes pull # Pull changes (fails if you have local changes) toes pull --force # Overwrite local changes ``` **`toes status`** — Show what would be pushed or pulled. ```bash toes status # Changes to push: # * index.tsx # + new-file.ts # - removed-file.ts ``` **`toes diff`** — Show a line-by-line diff of changed files. **`toes sync`** — Watch for changes and sync bidirectionally in real-time. Useful during development when editing on the server. **`toes clean`** — Remove local files that don't exist on the server. ```bash toes clean # Interactive confirmation toes clean --force # No confirmation toes clean --dry-run # Show what would be removed ``` **`toes stash`** — Stash local changes (like `git stash`). ```bash toes stash # Save local changes toes stash pop # Restore stashed changes toes stash list # List all stashes ``` ### Environment Variables **`toes env [name]`** — List environment variables for an app. ```bash toes env my-app # List app vars toes env -g # List global vars ``` **`toes env set [name] [value]`** — Set a variable. ```bash toes env set my-app API_KEY sk-123 # Set for an app toes env set my-app API_KEY=sk-123 # KEY=value format also works toes env set -g API_KEY sk-123 # Set globally (shared by all apps) ``` Setting a variable automatically restarts the app. **`toes env rm [name] `** — Remove a variable. ```bash toes env rm my-app API_KEY # Remove from an app toes env rm -g API_KEY # Remove global var ``` ### Versioning Every push creates a timestamped version. The server keeps the last 5 versions. **`toes versions [name]`** — List deployed versions. ```bash toes versions my-app # Versions for my-app: # # → 20260219-143022 2/19/2026, 2:30:22 PM (current) # 20260218-091500 2/18/2026, 9:15:00 AM # 20260217-160845 2/17/2026, 4:08:45 PM ``` **`toes history [name]`** — Show file changes between versions. **`toes rollback [name]`** — Rollback to a previous version. ```bash toes rollback my-app # Interactive version picker toes rollback my-app -v 20260218-091500 # Rollback to specific version ``` ### Cron Jobs Cron commands talk to the cron tool running on your Toes server. **`toes cron [app]`** — List all cron jobs, or jobs for a specific app. **`toes cron status `** — Show details for a specific job. ```bash toes cron status my-app:backup # my-app:backup ok # # Schedule: day # State: idle # Last run: 2h ago # Duration: 3s # Exit code: 0 # Next run: in 22h ``` **`toes cron run `** — Trigger a job immediately. ```bash toes cron run my-app:backup ``` **`toes cron log [target]`** — View cron logs. ```bash toes cron log # All cron logs toes cron log my-app # Cron logs for an app toes cron log my-app:backup # Logs for a specific job toes cron log -f # Follow logs ``` ### Metrics **`toes metrics [name]`** — Show CPU, memory, and disk usage. ```bash toes metrics # All apps toes metrics my-app # Single app ``` ### Sharing **`toes share [name]`** — Create a public tunnel to share an app over the internet. ```bash toes share my-app # ↗ Sharing my-app... https://abc123.trycloudflare.com ``` **`toes unshare [name]`** — Stop sharing an app. ### Configuration **`toes config`** — Show the current server URL and sync state. --- ## Environment Variables Toes injects these variables into every app process automatically: | Variable | Description | |----------|-------------| | `PORT` | Assigned port (3001-3100). Your app must listen on this port. | | `APPS_DIR` | Path to the apps directory on the server. | | `DATA_DIR` | Per-app data directory for persistent storage. | | `TOES_URL` | Base URL of the Toes server (e.g., `http://toes.local:3000`). | | `TOES_DIR` | Path to the Toes config directory. | You can set custom variables per-app or globally. Global variables are inherited by all apps. Per-app variables override globals. ```bash # Set per-app toes env set my-app OPENAI_API_KEY sk-123 # Set globally (shared by all apps) toes env set -g DATABASE_URL postgres://localhost/mydb ``` Access them in your app: ```tsx const apiKey = process.env.OPENAI_API_KEY ``` --- ## Health Checks Toes checks `GET /ok` on every app every 30 seconds. Your app must return a 2xx response. Three consecutive failures trigger an automatic restart with exponential backoff (1s, 2s, 4s, 8s, 16s, 32s). After 5 restart failures, the app is marked as errored and restart is disabled. The simplest health check: ```tsx app.get('/ok', c => c.text('ok')) ``` --- ## App Lifecycle Apps move through these states: ``` invalid → stopped → starting → running → stopping → stopped ↓ error ``` - **invalid** — Missing `package.json` or `scripts.toes`. Fix the config and start manually. - **stopped** — Not running. Start with `toes start` or the dashboard. - **starting** — Process spawned, waiting for `/ok` to return 200. Times out after 30 seconds. - **running** — Healthy and serving requests. - **stopping** — SIGTERM sent, waiting for process to exit. Escalates to SIGKILL after 10 seconds. - **error** — Crashed too many times. Start manually to retry. On startup, `bun install` runs automatically before the app's `scripts.toes` command. Apps are accessed via subdomain: `http://my-app.toes.local` or `http://my-app.localhost`. The Toes server proxies requests to the app's assigned port. --- ## Cron Jobs Place TypeScript files in a `cron/` directory inside your app: ```ts // cron/daily-cleanup.ts export const schedule = "day" export default async function() { console.log("Running daily cleanup") // Your job logic here } ``` The cron tool auto-discovers jobs by scanning `cron/*.ts` in all apps. New jobs are picked up within 60 seconds. ### Schedules | Value | When | |-------|------| | `1 minute` | Every minute | | `5 minutes` | Every 5 minutes | | `15 minutes` | Every 15 minutes | | `30 minutes` | Every 30 minutes | | `hour` | Top of every hour | | `noon` | 12:00 daily | | `midnight` / `day` | 00:00 daily | | `week` / `sunday` | 00:00 Sunday | | `monday` - `saturday` | 00:00 on that day | Jobs inherit the app's working directory and all environment variables. --- ## Data Persistence Use the filesystem for data storage. The `DATA_DIR` environment variable points to a per-app directory that persists across deployments and restarts: ```tsx import { join } from 'path' import { readFileSync, writeFileSync, existsSync } from 'fs' const DATA_DIR = process.env.DATA_DIR! function loadData(): MyData { const path = join(DATA_DIR, 'data.json') if (!existsSync(path)) return { items: [] } return JSON.parse(readFileSync(path, 'utf-8')) } function saveData(data: MyData) { writeFileSync(join(DATA_DIR, 'data.json'), JSON.stringify(data, null, 2)) } ``` `DATA_DIR` is separate from your app's code directory, so pushes and rollbacks won't affect stored data.