Skip to content
Hoody.com

The conventional route to a full-stack deployment runs through months of Terraform, Kubernetes manifests, CI/CD pipelines, reverse proxy configs, and SSL certificates, with no guarantee that staging matches production. On Hoody, the same result is two containers, a handful of HTTP calls, and a URL you can share the same day.

This guide builds 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, so there is no Docker, Nginx, or deploy script to manage. The pieces compose as URLs calling URLs.


Here is what you 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) │ │ │
└──────────────┘ └──────────────┘

The whole app is two containers in one project. Every arrow in the diagram is an HTTP call, and every box is a URL.


First, create a project to organize your full-stack app.

Terminal window
# Create the project
hoody projects create --alias "my-saas-app"
# Create the backend container
hoody containers create --project $PROJECT_ID \
--server-id $SERVER_ID \
--name "backend" \
--container-image "debian/13" \
--hoody-kit
# Create the frontend container
hoody containers create --project $PROJECT_ID \
--server-id $SERVER_ID \
--name "frontend" \
--container-image "debian/13" \
--hoody-kit

You now have two containers, each with the full Hoody Kit HTTP stack running. Creating them takes seconds.


The backend lives entirely inside hoody-exec: you write functions in files and they become HTTP endpoints. There is no Express, Fastify, or server configuration.

Use hoody-sqlite to set up your data layer:

Terminal window
# Create the users table (--create-db-if-missing creates app.db on first use)
hoody db exec-transaction -c $BACKEND_ID --db /hoody/databases/app.db --create-db-if-missing \
--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 table
hoody db exec-transaction -c $BACKEND_ID --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)"}]'

Use the KV store built into hoody-sqlite for session management; there is no Redis, Memcached, or third service to run:

Terminal window
# Store a session token
hoody kv set "session:abc123" -c $BACKEND_ID \
--db /hoody/databases/app.db \
--body '{"user_id": 1, "expires": "2026-04-01T00:00:00Z"}'

Create your API endpoint scripts. Each file becomes a URL automatically:

Terminal window
# Build the URL first so the embedded value is concrete
SQLITE_URL="https://$PROJECT_ID-$BACKEND_ID-sqlite-1.$SERVER.containers.hoody.com"
# Write the users endpoint
hoody exec scripts write -c $BACKEND_ID \
--path "api/users.ts" \
--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({ transaction: [{ query: \"INSERT INTO users (email, name) VALUES (?, ?)\", values: [email, name] }] })\n });\n return await result.json();\n}" \
--create-dirs

That script is now live at:

https://$PROJECT_ID-$BACKEND_ID-exec-1.$SERVER.containers.hoody.com/api/users

There is no deployment, build step, or restart: the file is the API.


Use hoody-terminal to scaffold a React app inside the frontend container:

Terminal window
# Bun ships with dev_kit (on by default); this line is only needed if it was disabled.
# --ephemeral auto-generates an isolated session; without it --terminal-id is required.
hoody terminal sessions exec -c $FRONTEND_ID --ephemeral \
--command "curl -fsSL https://bun.sh/install | bash && \
bun create vite my-app --template react-ts && \
cd my-app && bun install"

Configure the React app to call the backend API. The backend is just a URL, so there is no environment variable wiring or proxy configuration:

// src/api.ts (inside your React app)
const API_BASE = 'https://PROJECT_ID-BACKEND_ID-exec-1.SERVER.containers.hoody.com';
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();
}

Use hoody-daemon to run the dev server as a managed background process:

Terminal window
# Start the React dev server as a daemon
hoody daemon programs create -c $FRONTEND_ID \
--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.


Use proxy aliases to turn the long ID-based container URLs into clean production domains.

Terminal window
# Create alias for the frontend (React dev server on port 3000)
hoody proxy create \
--container-id $FRONTEND_ID \
--program http --port 3000 \
--alias "my-saas-app"
# Create alias for the API
hoody proxy create \
--container-id $BACKEND_ID \
--program exec --index 1 \
--alias "api-my-saas-app"

Now your app is live at https://my-saas-app.$SERVER.containers.hoody.com with the API at https://api-my-saas-app.$SERVER.containers.hoody.com. You can also connect your own domain; see Connect Your Domain.


Snapshot both containers before you go live:

Terminal window
# Snapshot backend
hoody snapshots create -c $BACKEND_ID \
--alias "pre-launch-backend"
# Snapshot frontend
hoody snapshots create -c $FRONTEND_ID \
--alias "pre-launch-frontend"

If something breaks after launch, restore both containers in seconds. The entire application (code, database, and configuration) rolls back to the moment you snapshotted.


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.<server>.containers.hoody.com │ │
│ │ → frontend:http:3000 │ │
│ │ api-my-saas-app.<server>.containers.hoody.com │ │
│ │ → backend:exec-1 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ SNAPSHOTS │ │
│ │ pre-launch-backend (entire backend state) │ │
│ │ pre-launch-frontend (entire frontend state) │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘

Every box is a URL and every arrow is an HTTP request. Each component can be snapshotted, shared, and composed with the others.


With the app running, your daily development looks like this:

  1. Open hoody-code (VS Code in browser) to edit frontend or backend files
  2. Open hoody-terminal side by side for running tests, checking logs
  3. Open hoody-display for live preview of the React app
  4. Query hoody-sqlite directly via its web UI to inspect data
  5. Snapshot before any risky change; restore in seconds if something breaks
  6. Share the URL with your team; they are instantly in your development environment

There is no local setup, no “works on my machine,” and no environment drift: the development environment is the production environment, separated only by a proxy alias.


When your app grows, the same architecture extends:

  • 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/shares/
  • 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: every service takes HTTP in, sends HTTP out, and is addressed by a URL.