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 built in and starts on the first request
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 transactions run -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)"}, {"statement": "CREATE TABLE IF NOT EXISTS webhook_events (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL, event_type TEXT NOT NULL, payload TEXT NOT NULL, received_at DATETIME NOT NULL)"}]'
# Insert sample data
hoody db transactions run -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
# The script is written verbatim, so pass real newlines, not a "\n" escape
hoody exec scripts write -c $CONTAINER_ID \
--path "admin/dashboard.ts" \
--content \
'// @mode serverless
// @cors reflective
// @timeout 5000
const SQLITE = "https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.com";
const stats = await fetch(SQLITE + "/api/v1/sqlite/db?db=/hoody/databases/app.db", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ transaction: [{ query: "SELECT COUNT(*) as total_users FROM users" }] })
}).then(r => r.json());
return { 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"

SDK-tab response (the CLI, HTTP, and URL variants return the single SQLite count response under statistics and omit recent_logins):

{
"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 00:00:00" },
{ "name": "Alice Chen", "email": "alice@company.com", "role": "admin", "status": "active", "last_login": "2026-03-03 00:00:00" },
{ "name": "Carol Kim", "email": "carol@company.com", "role": "user", "status": "suspended", "last_login": "2026-02-15 00:00:00" }
],
"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
# The script is written verbatim, so pass real newlines, not a "\n" escape
hoody exec scripts write -c $CONTAINER_ID \
--path "webhooks/stripe.ts" \
--content \
'// @mode serverless
// @cors *
// @timeout 10000
const SQLITE = "https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.com";
const NOTIFY = "https://PROJECT-CONTAINER-n-1.SERVER.containers.hoody.com";
const saved = await fetch(SQLITE + "/api/v1/sqlite/db?db=/hoody/databases/app.db", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
transaction: [{
statement: "INSERT INTO webhook_events (source, event_type, payload, received_at) VALUES (?, ?, ?, ?)",
values: ["stripe", req.body.type, JSON.stringify(req.body), new Date().toISOString()]
}]
})
});
if (!saved.ok) {
// Not stored: answer an error so the sender retries.
res.statusCode = 502;
return { received: false };
}
return { received: true, event: req.body.type };'

Point Stripe’s webhook URL at:

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

A webhook is acknowledged only once SQLite has stored it; if the write fails, the script answers 502 so the sender retries it. In the SDK-tab version, successful payments trigger a desktop notification on display 1; the CLI, HTTP, and URL versions only store and acknowledge the event. 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, serves them through hoody-exec, and—in the SDK variant—archives copies through 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"
# The script is written verbatim, so build it with real newlines.
# An unquoted heredoc still expands $SQLITE and $FILES.
SCRIPT=$(cat <<EOF
// @mode serverless
// @cors reflective
// @timeout 30000
const SQLITE = "$SQLITE";
const FILES = "$FILES";
const result = await fetch(SQLITE + "/api/v1/sqlite/db?db=/hoody/databases/app.db", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ transaction: [{ query: "SELECT name, email, role, status FROM users" }] })
}).then(r => r.json());
// A failed query, or one cut off at the 10,000-row limit, is refused rather than exported short.
const item = result.results?.[0];
if (!item?.success || item.truncated) {
res.statusCode = 502;
return { error: "the report query failed or returned more than 10,000 rows" };
}
const rows = item.resultSet;
const csv = "Name,Email,Role,Status\n" + rows.map(r => [r.name, r.email, r.role, r.status].join(",")).join("\n");
res.setHeader("Content-Type", "text/csv");
return csv;
EOF
)
hoody exec scripts write -c $CONTAINER_ID \
--path "reports/weekly-users.ts" \
--content "$SCRIPT"

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 SDK-tab script also saves reports through hoody-files, one file per UTC date (a second run on the same date replaces that day’s copy); the CLI, HTTP, and URL variants only return the CSV.

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; when the SDK-tab script is installed, it also saves a copy 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 set --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 set --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 aliases create --container-id $CONTAINER_ID --alias "ops-admin" --program exec
# Now accessible at: https://ops-admin.SERVER.containers.hoody.com/admin/dashboard

Not admin. That word, and api, system and internal, are reserved by the router but not by the create call: the alias is created and the hostname then resolves to nothing. See naming rules.

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.