Building a Full-Stack Application
Section titled “Building a Full-Stack Application”The old way: three months of Terraform, Kubernetes manifests, CI/CD pipelines, reverse proxy configs, SSL certificates, and a prayer that staging matches production.
The Hoody way: two containers, a handful of HTTP calls, and a URL you can share before lunch.
We are going to build a full-stack application from scratch — a React frontend, a TypeScript API backend, a SQLite database with session management, and a production domain. Everything runs inside containers where every service is already an HTTP endpoint. No Docker. No Nginx. No deploy scripts. Just URLs composing with URLs.
The Architecture
Section titled “The Architecture”Here is what we are building:
your-app.com (Proxy Alias) | ┌────────────┴────────────┐ v v ┌──────────────┐ ┌──────────────┐ │ Frontend │ │ Backend │ │ Container │ HTTP │ Container │ │ │ ──────> │ │ │ hoody-daemon│ │ hoody-exec │ │ (React app) │ │ (API routes)│ │ │ │ hoody-sqlite│ │ hoody-code │ │ (database) │ │ (VS Code) │ │ │ └──────────────┘ └──────────────┘Two containers. One project. Every arrow is an HTTP call. Every box is a URL.
Step 1: Create the Project and Containers
Section titled “Step 1: Create the Project and Containers”First, create a project to organize your full-stack app.
# Create the projecthoody projects create --alias "my-saas-app"
# Create the backend containerhoody containers create --project $PROJECT_ID \ --server-id $SERVER_ID \ --name "backend" \ --container-image "debian-12" \ --hoody-kit
# Create the frontend containerhoody containers create --project $PROJECT_ID \ --server-id $SERVER_ID \ --name "frontend" \ --container-image "debian-12" \ --hoody-kitimport { HoodyClient } from '@hoody-ai/hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
// Create the projectconst project = await client.api.projects.create({ alias: 'my-saas-app' });
// Create backend containerconst backend = await client.api.containers.create(project.data.id, { name: 'backend', server_id: SERVER_ID, container_image: 'debian-12', hoody_kit: true,});
// Create frontend containerconst frontend = await client.api.containers.create(project.data.id, { name: 'frontend', server_id: SERVER_ID, container_image: 'debian-12', hoody_kit: true,});
console.log('Backend:', backend.data.id);console.log('Frontend:', frontend.data.id);# Create the projectcurl -X POST "https://api.hoody.icu/api/v1/projects" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"alias": "my-saas-app"}'
# Create backend containercurl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "backend", "server_id": "'$SERVER_ID'", "container_image": "debian-12", "hoody_kit": true }'
# Create frontend containercurl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "frontend", "server_id": "'$SERVER_ID'", "container_image": "debian-12", "hoody_kit": true }'You now have two containers. Each has the full Hoody Kit HTTP stack running. Total time: seconds.
Step 2: Build the Backend API
Section titled “Step 2: Build the Backend API”The backend lives entirely inside hoody-exec. No Express. No Fastify. No server configuration. You write functions in files and they become HTTP endpoints.
Create the Database Schema
Section titled “Create the Database Schema”Use hoody-sqlite to set up your data layer:
# Create the users tablehoody db exec-transaction --db /hoody/databases/app.db \ --transaction '[{"statement": "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, name TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"}]'
# Create the posts tablehoody db exec-transaction --db /hoody/databases/app.db \ --transaction '[{"statement": "CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id), title TEXT NOT NULL, body TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"}]'import { HoodyClient } from '@hoody-ai/hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
const containerClient = await client.withContainer({ id: BACKEND_ID, project_id: PROJECT_ID, server: SERVER,});
// Call the SQLite service directly over HTTP — it's just another URL.// The `db` query param is required; create_db_if_missing=true creates app.db on first use.const sqliteUrl = `https://${PROJECT_ID}-${BACKEND_ID}-sqlite-1.${SERVER}.containers.hoody.icu`;
await fetch(`${sqliteUrl}/api/v1/sqlite/db?db=/hoody/databases/app.db&create_db_if_missing=true`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ transaction: [ { statement: `CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, name TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )` } ], })});
await fetch(`${sqliteUrl}/api/v1/sqlite/db?db=/hoody/databases/app.db`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ transaction: [ { statement: `CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id), title TEXT NOT NULL, body TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )` } ], })});# Create users table (create_db_if_missing=true creates app.db on first use)curl -X POST "https://$PROJECT_ID-$BACKEND_ID-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/db?db=/hoody/databases/app.db&create_db_if_missing=true" \ -H "Content-Type: application/json" \ -d '{ "transaction": [{"statement": "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, name TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"}] }'
# Create posts tablecurl -X POST "https://$PROJECT_ID-$BACKEND_ID-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/db?db=/hoody/databases/app.db" \ -H "Content-Type: application/json" \ -d '{ "transaction": [{"statement": "CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id), title TEXT NOT NULL, body TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"}] }'Set Up Session Storage with KV
Section titled “Set Up Session Storage with KV”Use the KV store built into hoody-sqlite for session management — no Redis, no Memcached, no third service:
# Store a session tokenhoody kv set "session:abc123" \ --db /hoody/databases/app.db \ --body '{"user_id": 1, "expires": "2026-04-01T00:00:00Z"}'import { HoodyClient } from '@hoody-ai/hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
const containerClient = await client.withContainer({ id: BACKEND_ID, project_id: PROJECT_ID, server: SERVER,});
await containerClient.sqlite.kvStore.set('session:abc123', JSON.stringify({ user_id: 1, expires: '2026-04-01T00:00:00Z' }), { db: '/hoody/databases/app.db' });# The request body IS the value (raw string), not a JSON wrapper. `db` is required.curl -X PUT "https://$PROJECT_ID-$BACKEND_ID-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/kv/session:abc123?db=/hoody/databases/app.db" \ -H "Content-Type: application/json" \ -d '{"user_id": 1, "expires": "2026-04-01T00:00:00Z"}'Write the API Routes
Section titled “Write the API Routes”Create your API endpoint scripts. Each file becomes a URL automatically:
# Write the users endpointhoody exec scripts write \ --path "api/users.ts" \ --content "// @mode serverless\n// @cors reflective\n// @timeout 5000\n\nconst SQLITE_URL = \"https://'$PROJECT_ID'-'$BACKEND_ID'-sqlite-1.'$SERVER'.containers.hoody.icu\";\n\nif (metadata.method === \"GET\") {\n const result = await fetch(SQLITE_URL + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ transaction: [{ query: \"SELECT * FROM users ORDER BY created_at DESC\" }] })\n });\n return await result.json();\n}\n\nif (metadata.method === \"POST\") {\n const { email, name } = req.body;\n const result = await fetch(SQLITE_URL + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ transaction: [{ query: \"INSERT INTO users (email, name) VALUES (?, ?)\", values: [email, name] }] })\n });\n return await result.json();\n}" \ --create-dirsimport { HoodyClient } from '@hoody-ai/hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
const containerClient = await client.withContainer({ id: BACKEND_ID, project_id: PROJECT_ID, server: SERVER,});
const sqliteUrl = `https://${PROJECT_ID}-${BACKEND_ID}-sqlite-1.${SERVER}.containers.hoody.icu`;
await containerClient.exec.scripts.write({ path: 'api/users.ts', content: `// @mode serverless// @cors reflective// @timeout 5000
const SQLITE_URL = "${sqliteUrl}";
if (metadata.method === "GET") { const result = await fetch(SQLITE_URL + "/api/v1/sqlite/db?db=/hoody/databases/app.db", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ transaction: [{ query: "SELECT * FROM users ORDER BY created_at DESC" }] }) }); return await result.json();}
if (metadata.method === "POST") { const { email, name } = req.body; const result = await fetch(SQLITE_URL + "/api/v1/sqlite/db?db=/hoody/databases/app.db", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ transaction: [{ query: "INSERT INTO users (email, name) VALUES (?, ?)", values: [email, name] }] }) }); return await result.json();} `, createDirs: true,});# Build the script payload from real env vars so the embedded URL is concrete.SQLITE_URL="https://$PROJECT_ID-$BACKEND_ID-sqlite-1.$SERVER.containers.hoody.icu"SCRIPT_CONTENT="// @mode serverless\n// @cors reflective\n// @timeout 5000\n\nconst SQLITE_URL = \"$SQLITE_URL\";\n\nif (metadata.method === \"GET\") {\n const result = await fetch(SQLITE_URL + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ transaction: [{ query: \"SELECT * FROM users ORDER BY created_at DESC\" }] })\n });\n return await result.json();\n}\n\nif (metadata.method === \"POST\") {\n const { email, name } = req.body;\n const result = await fetch(SQLITE_URL + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n transaction: [{ query: \"INSERT INTO users (email, name) VALUES (?, ?)\", values: [email, name] }]\n })\n });\n return await result.json();\n}"
curl -X POST "https://$PROJECT_ID-$BACKEND_ID-exec-1.$SERVER.containers.hoody.icu/api/v1/exec/scripts/write" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg p "api/users.ts" --arg c "$SCRIPT_CONTENT" '{path:$p,content:$c,createDirs:true}')"That script is now live at:
https://$PROJECT_ID-$BACKEND_ID-exec-1.$SERVER.containers.hoody.icu/api/usersNo deployment. No build step. No restart. The file IS the API.
Step 3: Scaffold the Frontend
Section titled “Step 3: Scaffold the Frontend”Use hoody-terminal to scaffold a React app inside the frontend container:
# Install Node.js and scaffold React app.# --ephemeral auto-generates an isolated session; without it --terminal-id is required.hoody terminal sessions exec --ephemeral \ --command "curl -fsSL https://bun.sh/install | bash && \ bun create vite my-app --template react-ts && \ cd my-app && bun install"import { HoodyClient } from '@hoody-ai/hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
const containerClient = await client.withContainer({ id: FRONTEND_ID, project_id: PROJECT_ID, server: SERVER,});
// Scaffold the React app. ephemeral=true spins up a guaranteed-unique// isolated PTY (no display/dbus, auto-cleanup) — ideal for one-shot commands.await containerClient.terminal.execution.execute({ command: `curl -fsSL https://bun.sh/install | bash && \ bun create vite my-app --template react-ts && \ cd my-app && bun install`,}, { ephemeral: true });# ephemeral=true creates a guaranteed-unique isolated PTY (no display/dbus, auto-cleanup).# Without it, terminal_id is required.curl -X POST "https://$PROJECT_ID-$FRONTEND_ID-terminal-1.$SERVER.containers.hoody.icu/api/v1/terminal/execute?ephemeral=true" \ -H "Content-Type: application/json" \ -d '{ "command": "curl -fsSL https://bun.sh/install | bash && bun create vite my-app --template react-ts && cd my-app && bun install" }'Connect Frontend to Backend
Section titled “Connect Frontend to Backend”Configure the React app to call the backend API. The backend is just a URL — no environment variable gymnastics, no proxy configuration:
// src/api.ts -- inside your React appconst API_BASE = 'https://PROJECT_ID-BACKEND_ID-exec-1.SERVER.containers.hoody.icu';
export async function getUsers() { const res = await fetch(`${API_BASE}/api/users`); return res.json();}
export async function createUser(email: string, name: string) { const res = await fetch(`${API_BASE}/api/users`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, name }), }); return res.json();}Serve with hoody-daemon
Section titled “Serve with hoody-daemon”Use hoody-daemon to run the dev server as a managed background process:
# Start the React dev server as a daemonhoody daemon programs create \ --name "react-dev" \ --command "cd /root/my-app && bun run dev --host 0.0.0.0 --port 3000" \ --user rootimport { HoodyClient } from '@hoody-ai/hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
const containerClient = await client.withContainer({ id: FRONTEND_ID, project_id: PROJECT_ID, server: SERVER,});
await containerClient.daemon.programs.add({ name: 'react-dev', command: 'cd /root/my-app && bun run dev --host 0.0.0.0 --port 3000', user: 'root',});curl -X POST "https://$PROJECT_ID-$FRONTEND_ID-daemon-1.$SERVER.containers.hoody.icu/api/v1/daemon/programs/add" \ -H "Content-Type: application/json" \ -d '{ "name": "react-dev", "command": "cd /root/my-app && bun run dev --host 0.0.0.0 --port 3000", "user": "root" }'Your React app is now running. View it live in hoody-display or access it through the container URL.
Step 4: Set Up a Production Domain
Section titled “Step 4: Set Up a Production Domain”Transform the cryptographic container URL into a clean production domain using proxy aliases.
# Create alias for the frontendhoody proxy create \ --container-id $FRONTEND_ID \ --program daemon --index 1 \ --alias "my-saas-app"
# Create alias for the APIhoody proxy create \ --container-id $BACKEND_ID \ --program exec --index 1 \ --alias "api-my-saas-app"import { HoodyClient } from '@hoody-ai/hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
// Frontend aliasawait client.api.proxyAliases.create({ container_id: FRONTEND_ID, alias: 'my-saas-app', program: 'daemon', index: 1,});
// API aliasawait client.api.proxyAliases.create({ container_id: BACKEND_ID, alias: 'api-my-saas-app', program: 'exec', index: 1,});# Frontend aliascurl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "container_id": "'$FRONTEND_ID'", "alias": "my-saas-app", "program": "daemon", "index": 1 }'
# API aliascurl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "container_id": "'$BACKEND_ID'", "alias": "api-my-saas-app", "program": "exec", "index": 1 }'Now your app is live at my-saas-app.hoody.icu with the API at api-my-saas-app.hoody.icu. You can also connect your own domain — see Connect Your Domain.
Step 5: Snapshot Before Going Live
Section titled “Step 5: Snapshot Before Going Live”Never deploy without a safety net. Snapshot both containers before you announce to the world:
# Snapshot backendhoody snapshots create -c $BACKEND_ID \ --alias "pre-launch-backend"
# Snapshot frontendhoody snapshots create -c $FRONTEND_ID \ --alias "pre-launch-frontend"import { HoodyClient } from '@hoody-ai/hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
await client.api.containers.createSnapshot(BACKEND_ID, { alias: 'pre-launch-backend',});
await client.api.containers.createSnapshot(FRONTEND_ID, { alias: 'pre-launch-frontend',});# Snapshot backendcurl -X POST "https://api.hoody.icu/api/v1/containers/$BACKEND_ID/snapshots" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"alias": "pre-launch-backend"}'
# Snapshot frontendcurl -X POST "https://api.hoody.icu/api/v1/containers/$FRONTEND_ID/snapshots" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"alias": "pre-launch-frontend"}'Something breaks post-launch? Restore both containers in seconds. The entire application — code, database, configuration — rolls back to the exact moment you snapshotted.
The Complete Picture
Section titled “The Complete Picture”Here is your full-stack application as a service composition:
┌──────────────────────────────────────────────────────────┐│ HOODY PROJECT ││ "my-saas-app" ││ ││ ┌────────────────────┐ ┌─────────────────────────┐ ││ │ FRONTEND CONTAINER│ │ BACKEND CONTAINER │ ││ │ │ │ │ ││ │ terminal-1 │ │ exec-1 ← API routes │ ││ │ (dev tools) │ │ (users.ts, posts.ts) │ ││ │ │ │ │ ││ │ daemon-1 │ │ sqlite-1 ← database │ ││ │ (React dev server)│ │ (users, posts, KV) │ ││ │ │ │ │ ││ │ code-1 │ │ terminal-1 │ ││ │ (VS Code in │ │ (maintenance) │ ││ │ browser) │ │ │ ││ │ │ │ daemon-1 │ ││ │ display-1 │ │ (background jobs) │ ││ │ (live preview) │ │ │ ││ └────────┬───────────┘ └──────────┬──────────────┘ ││ │ │ ││ │ HTTP calls │ ││ └─────────────>─────────────┘ ││ ││ ┌──────────────────────────────────────────────────┐ ││ │ PROXY ALIASES │ ││ │ my-saas-app.hoody.icu → frontend:daemon-1 │ ││ │ api-my-saas-app.hoody.icu → backend:exec-1 │ ││ └──────────────────────────────────────────────────┘ ││ ││ ┌──────────────────────────────────────────────────┐ ││ │ SNAPSHOTS │ ││ │ pre-launch-backend (entire backend state) │ ││ │ pre-launch-frontend (entire frontend state) │ ││ └──────────────────────────────────────────────────┘ │└──────────────────────────────────────────────────────────┘Every box is a URL. Every arrow is an HTTP request. Every component is snapshotable, shareable, and composable.
Development Workflow
Section titled “Development Workflow”With the app running, your daily development looks like this:
- Open hoody-code (VS Code in browser) to edit frontend or backend files
- Open hoody-terminal side by side for running tests, checking logs
- Open hoody-display for live preview of the React app
- Query hoody-sqlite directly via its web UI to inspect data
- Snapshot before any risky change — restore in seconds if something breaks
- Share the URL with your team — they are instantly in your development environment
No local setup. No “works on my machine.” No environment drift. The development environment IS the production environment, separated only by a proxy alias.
Scaling Up
Section titled “Scaling Up”When your app grows, Hoody grows with it:
- Add more containers for microservices — each is just another URL
- Use SQLite Drive to share databases across containers via
/hoody/databases/ - Use Shared Storage for cross-container files via
/hoody/shared/ - Add hoody-cron for scheduled jobs (backups, cleanups, reports)
- Add hoody-browser for automated testing of your frontend
- Point hoody-agent at your codebase and let AI handle pull requests
The architecture is the same at 1 container or 100. HTTP in, HTTP out, URLs all the way down.
What’s Next
Section titled “What’s Next”- The Vibe Coding Revolution — Let AI build your next feature while you watch
- Multiplayer by Default — Share your development environment with your team
- Rapid Internal Tools — Build admin dashboards and scripts in minutes
- Hoody Kit Overview — Deep dive into the 18 services powering your app