Skip to content
Hoody.com

In most companies, critical business processes run on spreadsheets, Slack messages, and manual copy-paste between systems. The admin dashboard that would take two months to build properly never gets built. A webhook processor that should exist gets replaced by someone checking email. Reports that could be automated are generated by hand every Friday.

These tools stay unbuilt because the overhead is out of proportion to the result: a server to provision, a database to configure, authentication to set up, a deployment pipeline to write, SSL certificates to manage, all for a tool three people will use.

On Hoody, an internal tool is a file. You write a function and hoody-exec serves it as an HTTP endpoint. You query the database through HTTP and share the URL with your team. The file is the tool, with no infrastructure, deploy step, or maintenance behind it.


Traditional internal toolHoody internal tool
Provision a serverAlready have one
Install a web frameworkWrite a function in a file
Set up a databasehoody-sqlite is already running
Configure authenticationProxy permissions
Write deployment scriptsFiles are live instantly
Manage SSL certificatesHandled by the proxy
Monitor uptimehoody-daemon auto-restarts
Schedule taskshoody-cron is already running

The time between needing a tool and having it drops from weeks to minutes.


The first build reads user data from SQLite and returns JSON. The frontend can be any HTML page, a React app, or a curl command.

Terminal window
# Create the users table with sample data (-c targets the container; --db is required;
# --create-db-if-missing creates app.db on first use)
hoody db exec-transaction -c $CONTAINER_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 NOT NULL, name TEXT NOT NULL, role TEXT DEFAULT '\''user'\'', status TEXT DEFAULT '\''active'\'', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME)"}]'
# Insert sample data
hoody db exec-transaction -c $CONTAINER_ID --db /hoody/databases/app.db \
--transaction '[{"statement": "INSERT INTO users (email, name, role, status, last_login) VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)", "values": ["alice@company.com", "Alice Chen", "admin", "active", "2026-03-03", "bob@company.com", "Bob Martinez", "user", "active", "2026-03-04", "carol@company.com", "Carol Kim", "user", "suspended", "2026-02-15"]}]'

Write the dashboard backend as a hoody-exec script:

Terminal window
hoody exec scripts write -c $CONTAINER_ID \
--path "admin/dashboard.ts" \
--content "// @mode serverless\n// @cors reflective\n// @timeout 5000\n\nconst SQLITE = \"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.com\";\n\nconst stats = await fetch(SQLITE + \"/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 COUNT(*) as total_users FROM users\" }] })\n}).then(r => r.json());\n\nreturn { statistics: stats, generated_at: new Date().toISOString() };"

Your dashboard API is now live:

Terminal window
curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/admin/dashboard"

Response:

{
"statistics": {
"total_users": 3,
"active_users": 2,
"suspended_users": 1,
"admin_count": 1,
"active_this_week": 2
},
"recent_logins": [
{ "name": "Bob Martinez", "email": "bob@company.com", "role": "user", "status": "active", "last_login": "2026-03-04" },
{ "name": "Alice Chen", "email": "alice@company.com", "role": "admin", "status": "active", "last_login": "2026-03-03" },
{ "name": "Carol Kim", "email": "carol@company.com", "role": "user", "status": "suspended", "last_login": "2026-02-15" }
],
"generated_at": "2026-03-04T12:00:00.000Z"
}

The working dashboard is one script file behind one URL; nothing else was provisioned.


The second build receives webhooks from external services, stores them in SQLite, and sends notifications.

Terminal window
hoody exec scripts write -c $CONTAINER_ID \
--path "webhooks/stripe.ts" \
--content "// @mode serverless\n// @cors *\n// @timeout 10000\n\nconst SQLITE = \"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.com\";\nconst NOTIFY = \"https://PROJECT-CONTAINER-n-1.SERVER.containers.hoody.com\";\n\nawait fetch(SQLITE + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n transaction: [{\n query: \"INSERT INTO webhook_events (source, event_type, payload, received_at) VALUES (?, ?, ?, ?)\",\n values: [\"stripe\", req.body.type, JSON.stringify(req.body), new Date().toISOString()]\n }]\n })\n});\n\nreturn { received: true, event: req.body.type };"

Point Stripe’s webhook URL at:

https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/webhooks/stripe

Every webhook is stored in SQLite. Successful payments trigger a desktop notification on the container’s display (display 1 in the script above). Each request is an isolated execution that writes straight to SQLite, so the database transaction is the source of truth and there is no shared in-memory state to corrupt.


The third build generates CSV reports from database queries and serves them via hoody-files.

Terminal window
SQLITE="https://$PROJECT_ID-$CONTAINER_ID-sqlite-1.$SERVER.containers.hoody.com"
FILES="https://$PROJECT_ID-$CONTAINER_ID-files-1.$SERVER.containers.hoody.com"
hoody exec scripts write -c $CONTAINER_ID \
--path "reports/weekly-users.ts" \
--content "// @mode serverless\n// @cors reflective\n// @timeout 30000\n\nconst SQLITE = \"$SQLITE\";\nconst FILES = \"$FILES\";\n\nconst result = await fetch(SQLITE + \"/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 name, email, role, status FROM users\" }] })\n}).then(r => r.json());\n\nconst rows = result.results?.[0]?.resultSet ?? result.results;\nconst csv = \"Name,Email,Role,Status\\n\" + rows.map(r => [r.name, r.email, r.role, r.status].join(\",\")).join(\"\\n\");\n\nres.setHeader(\"Content-Type\", \"text/csv\");\nreturn csv;"

Hit the URL and download the report:

Terminal window
curl -o report.csv "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/reports/weekly-users"

The script also saves each report to the filesystem via hoody-files, so past reports accumulate into an archive.

Run the report automatically every Friday:

Terminal window
hoody cron entries create root \
-c $CONTAINER_ID \
--schedule "0 9 * * 5" \
--command "curl -s https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/reports/weekly-users > /dev/null" \
--comment "Weekly user report generation"

Every Friday at 9 AM, the report generates and saves to the filesystem. The schedule came from one HTTP call, not from editing a crontab or maintaining a server.


Internal tools should not be public. Lock them down:

Terminal window
# Build the document one field at a time with the granular commands.
# Every successful write bumps file_version, so read a fresh ETag before each
# mutation; a stale --if-match is rejected with 412.
v() { hoody containers proxy permissions get -c $CONTAINER_ID -o json | jq -r '.file_version'; }
# Password protect the container
hoody containers proxy groups password set -c $CONTAINER_ID \
--group-name team \
--auth-username team --auth-password 'internal-tools-2026' \
--salt unique-salt --algorithm sha256 \
--if-match "file:v$(v)"
# Grant the services the group may reach, one program per call
for program in terminal files display exec sqlite; do
hoody containers proxy groups permissions set -c $CONTAINER_ID \
--group-name team --program $program --access true --if-match "file:v$(v)"
done
hoody containers proxy groups permissions set -c $CONTAINER_ID \
--group-name team --program http --access '[8080]' --if-match "file:v$(v)"
# Everything not granted above is denied
hoody containers proxy default --default deny -c $CONTAINER_ID --if-match "file:v$(v)"
# Alternative: reset the document, then restrict to office IP
hoody containers proxy permissions delete -c $CONTAINER_ID --if-match "file:v$(v)" -y
hoody containers proxy groups ip set -c $CONTAINER_ID \
--group-name office --range 203.0.113.0/24 --if-match "file:v$(v)"
for program in terminal files display exec sqlite; do
hoody containers proxy groups permissions set -c $CONTAINER_ID \
--group-name office --program $program --access true --if-match "file:v$(v)"
done
hoody containers proxy groups permissions set -c $CONTAINER_ID \
--group-name office --program http --access '[8080]' --if-match "file:v$(v)"
hoody containers proxy default --default deny -c $CONTAINER_ID --if-match "file:v$(v)"

Every service granted to the team group (exec endpoints, the SQLite UI, terminal access) now requires the password, and anything you did not grant is denied by the default: deny policy.


To distribute a tool, send its URL.

Admin Dashboard: https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/admin/dashboard
Webhook Processor: https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/webhooks/stripe
Weekly Report: https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/reports/weekly-users
SQLite Web UI: https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.com
File Browser: https://PROJECT-CONTAINER-files-1.SERVER.containers.hoody.com

Or create shorter aliases:

Terminal window
hoody proxy create --container-id $CONTAINER_ID --alias "admin" --program exec
# Now accessible at: https://admin.SERVER.containers.hoody.com/admin/dashboard

Whoever opens the URL gets the tool, with no client to install, no deployment on their side, and no access procedure to learn beyond opening a browser.


Every internal tool on Hoody follows the same pattern:

  1. Write a hoody-exec script: the logic lives in a file that becomes a URL.
  2. Use hoody-sqlite for data: queries and storage go through HTTP.
  3. Use hoody-notifications for alerts: desktop notifications appear on the container display when events occur.
  4. Use hoody-files for output: reports, exports, and archives land on the filesystem.
  5. Use hoody-cron for scheduling: entries created over HTTP run the script unattended.
  6. Use proxy permissions for access: restrict by password or IP range.
  7. Share the URL: sending it is the distribution step.

None of it involves a server to manage, a framework to learn, a deployment pipeline, SSL certificate renewals, Docker images, or Kubernetes manifests. The functions, the database, the file storage, and the schedule are all reached the same way: as URLs.