Rapid Internal Tools
Section titled “Rapid Internal Tools”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.
What Hoody replaces
Section titled “What Hoody replaces”| 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 time between needing a tool and having it drops from weeks to minutes.
Build an admin dashboard
Section titled “Build an admin dashboard”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.
Step 1: Create the data
Section titled “Step 1: Create the data”# 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 datahoody 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"]}]'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER,});
// Seed the table by POSTing a transaction to the SQLite service URLconst sqliteUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-sqlite-1.${SERVER}.containers.hoody.com`;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.com/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)"}] }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Creates the users table in app.db through hoody-sqlite. The dashboard and report scripts below both query this table, so run this first.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-sqlite-1.SERVER.containers.hoody.com/api/v1/sqlite/db?db=/hoody/databases/app.db%26create_db_if_missing=true&method=POST&json={"transaction":[{"statement":"CREATE%20TABLE%20IF%20NOT%20EXISTS%20users%20(id%20INTEGER%20PRIMARY%20KEY%20AUTOINCREMENT,%20email%20TEXT%20NOT%20NULL,%20name%20TEXT%20NOT%20NULL,%20role%20TEXT%20DEFAULT%20'user',%20status%20TEXT%20DEFAULT%20'active',%20created_at%20DATETIME%20DEFAULT%20CURRENT_TIMESTAMP,%20last_login%20DATETIME)"}]}&response=transparent Step 2: Write the dashboard API
Section titled “Step 2: Write the dashboard API”Write the dashboard backend as a hoody-exec script:
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() };"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', 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.com`;
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.results?.[0]?.resultSet?.[0] ?? stats.results?.[0], recent_logins: recent.results?.[0]?.resultSet ?? recent.results, generated_at: new Date().toISOString()}; `, createDirs: true,});curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-exec-1.$SERVER.containers.hoody.com/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.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() };", "createDirs": true }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Writes the dashboard script to admin/dashboard.ts via hoody-exec; it is live at /admin/dashboard as soon as the write returns.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.com/api/v1/exec/scripts/write&method=POST&json={"path":"admin/dashboard.ts","content":"//%20@mode%20serverless\n//%20@cors%20reflective\n//%20@timeout%205000\n\nconst%20SQLITE%20=%20\"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.com\";\n\nconst%20stats%20=%20await%20fetch(SQLITE%20%2B%20\"/api/v1/sqlite/db?db=/hoody/databases/app.db\",%20{\n%20%20method:%20\"POST\",\n%20%20headers:%20{%20\"Content-Type\":%20\"application/json\"%20},\n%20%20body:%20JSON.stringify({%20transaction:%20[{%20query:%20\"SELECT%20COUNT(*)%20as%20total_users%20FROM%20users\"%20}]%20})\n}).then(r%20=>%20r.json());\n\nreturn%20{%20statistics:%20stats,%20generated_at:%20new%20Date().toISOString()%20};","createDirs":true}&response=transparent Step 3: Call the endpoint
Section titled “Step 3: Call the endpoint”Your dashboard API is now live:
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.
Build a webhook processor
Section titled “Build a webhook processor”The second build receives webhooks from external services, stores them in SQLite, and sends notifications.
Write the webhook script
Section titled “Write the webhook script”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 };"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', 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.com`;const notificationUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-n-1.${SERVER}.containers.hoody.com`;
await containerClient.exec.scripts.write({ path: 'webhooks/stripe.ts', content: `// @mode serverless// @cors *// @timeout 10000
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: "1", urgency: "normal" }) });}
return { received: true, event: req.body.type }; `, createDirs: true,});curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-exec-1.$SERVER.containers.hoody.com/api/v1/exec/scripts/write" \ -H "Content-Type: application/json" \ -d '{ "path": "webhooks/stripe.ts", "content": "// @mode serverless\n// @cors *\n// @timeout 10000\n\nconst SQLITE = \"https://PROJECT-CONTAINER-sqlite-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 };", "createDirs": true }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Writes the webhook script to webhooks/stripe.ts via hoody-exec. This body only stores the event; the payment-notification branch shown in the SDK tab is not part of this request.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.com/api/v1/exec/scripts/write&method=POST&json={"path":"webhooks/stripe.ts","content":"//%20@mode%20serverless\n//%20@cors%20*\n//%20@timeout%2010000\n\nconst%20SQLITE%20=%20\"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.com\";\n\nawait%20fetch(SQLITE%20%2B%20\"/api/v1/sqlite/db?db=/hoody/databases/app.db\",%20{\n%20%20method:%20\"POST\",\n%20%20headers:%20{%20\"Content-Type\":%20\"application/json\"%20},\n%20%20body:%20JSON.stringify({\n%20%20%20%20transaction:%20[{\n%20%20%20%20%20%20query:%20\"INSERT%20INTO%20webhook_events%20(source,%20event_type,%20payload,%20received_at)%20VALUES%20(?,%20?,%20?,%20?)\",\n%20%20%20%20%20%20values:%20[\"stripe\",%20req.body.type,%20JSON.stringify(req.body),%20new%20Date().toISOString()]\n%20%20%20%20}]\n%20%20})\n});\n\nreturn%20{%20received:%20true,%20event:%20req.body.type%20};","createDirs":true}&response=transparent Point Stripe’s webhook URL at:
https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/webhooks/stripeEvery 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.
Build a report generator
Section titled “Build a report generator”The third build generates CSV reports from database queries and serves them via hoody-files.
Write the report script
Section titled “Write the report script”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;"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', 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.com`;const filesUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-files-1.${SERVER}.containers.hoody.com`;
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.results?.[0]?.resultSet ?? result.results;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.com/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.com\";\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;", "createDirs": true }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Writes the report script to reports/weekly-users.ts via hoody-exec. This version returns the CSV directly; it does not also upload it to hoody-files the way the SDK example does.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.com/api/v1/exec/scripts/write&method=POST&json={"path":"reports/weekly-users.ts","content":"//%20@mode%20serverless\n//%20@cors%20reflective\n//%20@timeout%2030000\n\nconst%20SQLITE%20=%20\"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.com\";\n\nconst%20result%20=%20await%20fetch(SQLITE%20%2B%20\"/api/v1/sqlite/db?db=/hoody/databases/app.db\",%20{\n%20%20method:%20\"POST\",\n%20%20headers:%20{%20\"Content-Type\":%20\"application/json\"%20},\n%20%20body:%20JSON.stringify({%20transaction:%20[{%20query:%20\"SELECT%20name,%20email,%20role,%20status%20FROM%20users\"%20}]%20})\n}).then(r%20=>%20r.json());\n\nconst%20rows%20=%20result.results?.[0]?.resultSet%20??%20result.results;\nconst%20csv%20=%20\"Name,Email,Role,Status\\n\"%20%2B%20rows.map(r%20=>%20[r.name,%20r.email,%20r.role,%20r.status].join(\",\")).join(\"\\n\");\n\nres.setHeader(\"Content-Type\",%20\"text/csv\");\nreturn%20csv;","createDirs":true}&response=transparent Hit the URL and download the report:
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.
Schedule the report with hoody-cron
Section titled “Schedule the report with hoody-cron”Run the report automatically every Friday:
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"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', 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.com`;
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.com/users/root/entries" \ -H "Content-Type: application/json" \ -d '{ "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" }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Creates the cron entry that runs the report script every Friday at 9 AM.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-cron-1.SERVER.containers.hoody.com/users/root/entries&method=POST&json={"schedule":"0%209%20*%20*%205","command":"curl%20-s%20https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/reports/weekly-users%20>%20/dev/null","comment":"Weekly%20user%20report%20generation"}&response=transparent 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.
Restrict access with proxy permissions
Section titled “Restrict access with proxy permissions”Internal tools should not be public. Lock them down:
# 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 containerhoody 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 callfor 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 deniedhoody containers proxy default --default deny -c $CONTAINER_ID --if-match "file:v$(v)"
# Alternative: reset the document, then restrict to office IPhoody 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)"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Writes need an If-Match precondition. Read the current file_version from a// prior GET (it is in the response body as `data.file_version`) and pass the// ETag as `ifMatch`, e.g. 'file:v1'.await client.api.proxyPermissionsContainer.replace(CONTAINER_ID, { project: PROJECT_ID, container: CONTAINER_ID, groups: { team: { type: 'password', username: 'team', password: 'internal-tools-2026', salt: 'unique-salt' } }, permissions: { team: { terminal: true, files: true, display: true, exec: true, sqlite: true, http: [8080] } }, default: 'deny'}, { ifMatch: 'file:v1' });# Writes require an If-Match precondition (read the current file_version via GET first)curl -X PUT "https://api.hoody.com/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","username":"team","password":"internal-tools-2026","salt":"unique-salt"}},"permissions":{"team":{"terminal":true,"files":true,"display":true,"exec":true,"sqlite":true,"http":[8080]}},"default":"deny"}'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Replaces the container’s whole permissions document in one write. Read the current file_version from a prior GET first and put it in If-Match — a stale value is rejected with 412.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/proxy/permissions&method=PUT&bearer_token=TOKEN&header=If-Match:%20file:v1&json={"project":"PROJECT_ID","container":"CONTAINER_ID","groups":{"team":{"type":"password","username":"team","password":"internal-tools-2026","salt":"unique-salt"}},"permissions":{"team":{"terminal":true,"files":true,"display":true,"exec":true,"sqlite":true,"http":[8080]}},"default":"deny"}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
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.
Share the URLs
Section titled “Share the URLs”To distribute a tool, send its URL.
Admin Dashboard: https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/admin/dashboardWebhook Processor: https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/webhooks/stripeWeekly Report: https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/reports/weekly-usersSQLite Web UI: https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.comFile Browser: https://PROJECT-CONTAINER-files-1.SERVER.containers.hoody.comOr create shorter aliases:
hoody proxy create --container-id $CONTAINER_ID --alias "admin" --program exec# Now accessible at: https://admin.SERVER.containers.hoody.com/admin/dashboardWhoever 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.
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 go through HTTP.
- Use hoody-notifications for alerts: desktop notifications appear on the container display when events occur.
- Use hoody-files for output: reports, exports, and archives land on the filesystem.
- Use hoody-cron for scheduling: entries created over HTTP run the script unattended.
- Use proxy permissions for access: restrict by password or IP range.
- 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.
What’s Next
Section titled “What’s Next”- Building a Full-Stack Application: scale an internal tool into a customer-facing product
- Deploying Autonomous AI Agents: let an AI agent build the next tool
- Hoody Exec Deep Dive: the script-to-API primitive in detail
- SQLite via HTTP: advanced database operations