Rapid Internal Tools
Section titled “Rapid Internal Tools”Every company has the same dirty secret: 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. The webhook processor that should exist gets replaced by someone checking email. The report that should be automated is generated by hand every Friday.
These tools do not get built because the overhead of building them is absurd. Spin up a server. Configure a database. Set up authentication. Write a deployment pipeline. Manage SSL certificates. All for an internal tool that three people use.
On Hoody, an internal tool is a file. You write a function, it becomes an HTTP endpoint. You query the database through HTTP. You share the URL with your team. Done. No infrastructure. No deployment. No maintenance. The file IS the tool.
Why Internal Tools Are Perfect for Hoody
Section titled “Why Internal Tools Are Perfect for Hoody”| Traditional Internal Tool | Hoody Internal Tool |
|---|---|
| Provision a server | Already have one |
| Install a web framework | Write a function in a file |
| Set up a database | hoody-sqlite is already running |
| Configure authentication | Proxy permissions |
| Write deployment scripts | Files are live instantly |
| Manage SSL certificates | Handled by the proxy |
| Monitor uptime | hoody-daemon auto-restarts |
| Schedule tasks | hoody-cron is already running |
The distance between “I need this tool” and “this tool exists” collapses from weeks to minutes.
Build 1: Admin Dashboard
Section titled “Build 1: Admin Dashboard”An admin dashboard that reads user data from SQLite and returns JSON. The frontend can be any HTML page, a React app, or even a curl command.
Step 1: Create the Data
Section titled “Step 1: Create the Data”# Create the users table with sample data (--db is required; --create-db-if-missing creates app.db on first use)hoody db exec-transaction --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 datahoody db exec-transaction --db /hoody/databases/app.db \ --transaction '[{"statement": "INSERT INTO users (email, name, role, status, last_login) 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\")"}]'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: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER,});
// SDK executeTransaction has a known body-param bug — using fetch for nowconst sqliteUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-sqlite-1.${SERVER}.containers.hoody.icu`;const notificationUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-n-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 NOT NULL, name TEXT NOT NULL, role TEXT DEFAULT 'user', status TEXT DEFAULT 'active', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME )` } ], })});curl -X POST "https://$PROJECT_ID-$CONTAINER_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 NOT NULL, name TEXT NOT NULL, role TEXT DEFAULT '\''user'\'', status TEXT DEFAULT '\''active'\'', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME)"}] }'Step 2: Write the Dashboard API
Section titled “Step 2: Write the Dashboard API”Create a hoody-exec script that serves as the dashboard backend:
hoody exec scripts write \ --path "admin/dashboard.ts" \ --content "// @mode serverless\n// @cors reflective\n// @timeout 5000\n\nconst SQLITE = \"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu\";\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() };" \ --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: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER,});
await containerClient.exec.scripts.write({ path: 'admin/dashboard.ts', content: `// @mode serverless// @cors reflective// @timeout 5000
const SQLITE = "${sqliteUrl}";
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, SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_users, SUM(CASE WHEN status = 'suspended' THEN 1 ELSE 0 END) as suspended_users, SUM(CASE WHEN role = 'admin' THEN 1 ELSE 0 END) as admin_count, SUM(CASE WHEN last_login > datetime('now', '-7 days') THEN 1 ELSE 0 END) as active_this_week FROM users\` }] })}).then(r => r.json());
const recent = 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, last_login FROM users ORDER BY last_login DESC LIMIT 10" }] })}).then(r => r.json());
return { statistics: stats.data?.[0] || stats, recent_logins: recent.data || recent, generated_at: new Date().toISOString()}; `, createDirs: true,});curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-exec-1.$SERVER.containers.hoody.icu/api/v1/exec/scripts/write" \ -H "Content-Type: application/json" \ -d '{ "path": "admin/dashboard.ts", "content": "// @mode serverless\n// @cors reflective\n// @timeout 5000\n\nconst SQLITE = \"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu\";\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() };", "createDirs": true }'Step 3: Use It
Section titled “Step 3: Use It”Your dashboard API is now live:
curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/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" } ], "generated_at": "2026-03-04T12:00:00.000Z"}From nothing to a working admin dashboard API: one file, one URL, zero infrastructure.
Build 2: Webhook Processor
Section titled “Build 2: Webhook Processor”Receive webhooks from external services, store them in SQLite, and send notifications.
The Script
Section titled “The Script”hoody exec scripts write \ --path "webhooks/stripe.ts" \ --content "// @mode serverless\n// @cors *\n// @timeout 10000\n// @concurrent false\n\nconst SQLITE = \"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu\";\nconst NOTIFY = \"https://PROJECT-CONTAINER-n-1.SERVER.containers.hoody.icu\";\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 };" \ --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: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER,});
const sqliteUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-sqlite-1.${SERVER}.containers.hoody.icu`;const notificationUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-n-1.${SERVER}.containers.hoody.icu`;
await containerClient.exec.scripts.write({ path: 'webhooks/stripe.ts', content: `// @mode serverless// @cors *// @timeout 10000// @concurrent false
const SQLITE = "${sqliteUrl}";const NOTIFY = "${notificationUrl}";
await fetch(SQLITE + "/api/v1/sqlite/db?db=/hoody/databases/app.db", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ transaction: [{ query: "INSERT INTO webhook_events (source, event_type, payload, received_at) VALUES (?, ?, ?, ?)", values: ["stripe", req.body.type, JSON.stringify(req.body), new Date().toISOString()] }] })});
if (req.body.type === "payment_intent.succeeded") { const amount = req.body.data.object.amount / 100; await fetch(NOTIFY + "/api/v1/notifications/notify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ summary: "Payment Received", body: amount + " " + req.body.data.object.currency.toUpperCase() + " payment successful", display: "0", urgency: "normal" }) });}
return { received: true, event: req.body.type }; `, createDirs: true,});curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-exec-1.$SERVER.containers.hoody.icu/api/v1/exec/scripts/write" \ -H "Content-Type: application/json" \ -d '{ "path": "webhooks/stripe.ts", "content": "// @mode serverless\n// @cors *\n// @timeout 10000\n// @concurrent false\n\nconst SQLITE = \"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu\";\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 };", "createDirs": true }'Point Stripe’s webhook URL at:
https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/webhooks/stripeEvery webhook is stored in SQLite. Payment successes and failures trigger push notifications to your phone. The @concurrent false magic comment ensures webhooks are processed one at a time — no race conditions, no duplicate processing.
Build 3: Report Generator
Section titled “Build 3: Report Generator”Generate CSV reports from database queries and serve them via hoody-files.
The Script
Section titled “The Script”SQLITE="https://$PROJECT_ID-$CONTAINER_ID-sqlite-1.$SERVER.containers.hoody.icu"FILES="https://$PROJECT_ID-$CONTAINER_ID-files-1.$SERVER.containers.hoody.icu"
hoody exec scripts write \ --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.data || result;\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;" \ --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: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER,});
const sqliteUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-sqlite-1.${SERVER}.containers.hoody.icu`;const filesUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-files-1.${SERVER}.containers.hoody.icu`;
await containerClient.exec.scripts.write({ path: 'reports/weekly-users.ts', content: `// @mode serverless// @cors reflective// @timeout 30000
const SQLITE = "${sqliteUrl}";const FILES = "${filesUrl}";
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, created_at, last_login FROM users ORDER BY last_login DESC" }] })}).then(r => r.json());
const rows = result.data || result;const headers = "Name,Email,Role,Status,Created,Last Login";const csvRows = rows.map(r => [r.name, r.email, r.role, r.status, r.created_at, r.last_login].join(","));const csv = headers + "\\n" + csvRows.join("\\n");
// Upload is PUT /api/v1/files/{path} with the raw file content as the body.const filename = "weekly-report-" + new Date().toISOString().split("T")[0] + ".csv";await fetch(FILES + "/api/v1/files/hoody/storage/reports/" + filename, { method: "PUT", headers: { "Content-Type": "text/csv" }, body: csv});
res.setHeader("Content-Type", "text/csv");res.setHeader("Content-Disposition", "attachment; filename=" + filename);return csv; `, createDirs: true,});curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-exec-1.$SERVER.containers.hoody.icu/api/v1/exec/scripts/write" \ -H "Content-Type: application/json" \ -d '{ "path": "reports/weekly-users.ts", "content": "// @mode serverless\n// @cors reflective\n// @timeout 30000\n\nconst SQLITE = \"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu\";\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.data || result;\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;", "createDirs": true }'Hit the URL and download the report:
curl -o report.csv "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/reports/weekly-users"The report is also saved to the filesystem via hoody-files, creating an archive of historical reports.
Automate with hoody-cron
Section titled “Automate with hoody-cron”Run the report automatically every Friday:
hoody cron entries create root \ --schedule "0 9 * * 5" \ --command "curl -s https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/reports/weekly-users > /dev/null" \ --comment "Weekly user report generation"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: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER,});
const execUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-exec-1.${SERVER}.containers.hoody.icu`;
await containerClient.cron.entries.create('root', { schedule: '0 9 * * 5', // Every Friday at 9 AM command: `curl -s ${execUrl}/reports/weekly-users > /dev/null`, comment: 'Weekly user report generation',});curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-cron-1.$SERVER.containers.hoody.icu/users/root/entries" \ -H "Content-Type: application/json" \ -d '{ "schedule": "0 9 * * 5", "command": "curl -s https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/reports/weekly-users > /dev/null", "comment": "Weekly user report generation" }'Every Friday at 9 AM, the report generates and saves to the filesystem. No crontab editing. No server maintenance. An HTTP call configured the schedule.
Access Control with Proxy Permissions
Section titled “Access Control with Proxy Permissions”Internal tools should not be public. Lock them down:
# Password protect the container# Writes are optimistic-concurrency guarded: pass the current file_version via --if-matchhoody containers proxy permissions replace -c $CONTAINER_ID \ --project $PROJECT_ID \ --if-match "$(hoody containers proxy permissions get --container $CONTAINER_ID --field file_version)" \ --groups '{"team": {"type": "password", "password": "internal-tools-2026"}}' \ --permissions '{}'
# Or restrict to office IPhoody containers proxy permissions replace -c $CONTAINER_ID \ --project $PROJECT_ID \ --if-match "$(hoody containers proxy permissions get --container $CONTAINER_ID --field file_version)" \ --groups '{"office": {"type": "ip", "range": "203.0.113.0/24"}}' \ --permissions '{}'import { HoodyClient } from '@hoody-ai/hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
// Writes need an If-Match precondition (the current file_version is returned// as a header by GET). Pass it via the `ifMatch` option, e.g. { ifMatch: 'file:v1' }.await client.api.proxyPermissionsContainer.replace(CONTAINER_ID, { project: PROJECT_ID, container: CONTAINER_ID, groups: { team: { type: 'password', password: 'internal-tools-2026' } }, permissions: { team: { terminal: true, files: true, display: true, exec: true, sqlite: true, http: true } }, default: 'deny'}, { ifMatch: 'file:v1' });# Writes require an If-Match precondition (read the current file_version via GET first)curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v1" \ -d '{"project":"'$PROJECT_ID'","container":"'$CONTAINER_ID'","groups":{"team":{"type":"password","password":"internal-tools-2026"}},"permissions":{"team":{"terminal":true,"files":true,"display":true,"exec":true,"sqlite":true,"http":true}},"default":"deny"}'Now all services — exec endpoints, SQLite UI, terminal access — require the password. One setting protects everything.
Share with Your Team
Section titled “Share with Your Team”Share the URL. That is the entire distribution process.
Admin Dashboard: https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/admin/dashboardWebhook Processor: https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/webhooks/stripeWeekly Report: https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/reports/weekly-usersSQLite Web UI: https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icuFile Browser: https://PROJECT-CONTAINER-files-1.SERVER.containers.hoody.icuOr create clean aliases:
hoody proxy create --container-id $CONTAINER_ID --program exec --index 1 --alias "admin"Your team opens the URL. The tool is there. No deployment, no installation, no training on how to access it. If they can open a browser, they can use the tool.
The Pattern
Section titled “The Pattern”Every internal tool on Hoody follows the same pattern:
- Write a hoody-exec script — The logic lives in a file that becomes a URL
- Use hoody-sqlite for data — Queries and storage through HTTP
- Use hoody-notifications for alerts — Push notifications when events occur
- Use hoody-files for output — Reports, exports, archives
- Use hoody-cron for scheduling — Automated execution on a schedule
- Use proxy permissions for access — Password or IP restriction
- Share the URL — Distribution complete
No servers to manage. No frameworks to learn. No deployment pipelines to configure. No SSL certificates to renew. No Docker images to build. No Kubernetes manifests to write.
Just functions that become URLs, talking to databases that are URLs, saving files that are URLs, on a schedule that is configured via URL.
What’s Next
Section titled “What’s Next”- Building a Full-Stack Application — Scale from internal tool to customer-facing product
- Deploying Autonomous AI Agents — Let AI build your next internal tool
- Hoody Exec Deep Dive — Master the script-to-API primitive
- SQLite via HTTP — Advanced database operations