Apps
Publish a server app once and reach it at a stable public URL — no infrastructure to manage. CloudClawer runs it inside your existing sandbox using the same slot mechanism as custom tools.
App types
handler.mjsindex.html (in zip)server.mjs (in zip)This guide covers server web apps — the most flexible type. Use them for REST APIs, webhook receivers, dashboards, or any HTTP service you want to ship without standing up your own infrastructure.
How it works
When a request arrives at https://api.cloudclawer.com/apps/{appId}/web/, the platform:
- Looks up the app's publisher username
- Claims a sandbox slot (port 8081–8089) from the publisher's running container
- Downloads
bundle.zipvia a presigned S3 URL and passes it to the launcher - Calls the launcher's
/invokeendpoint with the HTTP request as input - Translates the handler's return value back into an HTTP response
The server.mjs contract
Your entry file exports a default async function. It receives the HTTP request as a plain object and must return a response object.
Input
{
method: string; // "GET" | "POST" | "PUT" | "DELETE" | ...
path: string; // e.g. "/api/users/42" (relative to /apps/{id}/web)
query: Record<string, string>; // parsed query string
headers: Record<string, string>; // request headers (host, authorization stripped)
body: string; // raw request body as a string
}Output
{
status: number; // HTTP status code, e.g. 200
headers?: Record<string, string>; // optional response headers
body: string; // response body as a string
}Minimal example
// server.mjs
export default async function handler(input) {
return {
status: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({ hello: "world", path: input.path }),
};
}Echo server (good for testing)
// server.mjs
export default async function handler(input) {
return {
status: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({
method: input.method,
path: input.path,
query: input.query,
body: input.body,
}),
};
}Router example
// server.mjs
export default async function handler(input) {
const { method, path, body } = input;
if (method === "GET" && path === "/health") {
return { status: 200, headers: { "content-type": "text/plain" }, body: "ok" };
}
if (method === "POST" && path === "/echo") {
return { status: 200, headers: { "content-type": "application/json" }, body };
}
return { status: 404, body: "Not found" };
}Manifest
Add a web field to cloudclawer.json to declare a server web app:
{
"manifestVersion": "1",
"name": "my-server-app",
"displayName": "My Server App",
"description": "A minimal HTTP server app",
"version": "1.0.0",
"author": "your-username",
"entrypoint": "handler.mjs",
"execution": {
"lifecycle": "per-call",
"timeout_ms": 30000,
"input_schema": { "type": "object", "properties": {} }
},
"permissions": {},
"web": {
"entrypoint": "server.mjs",
"type": "server"
}
}Web field reference
| Field | Type | Required | Description |
|---|---|---|---|
web.type | "server" | "static" | yes | Set to "server" for a live HTTP handler |
web.entrypoint | string | yes | For server type: "server.mjs". For static: default file, e.g. "index.html" |
Publishing a server app
Set your API key once, then follow these six steps. Replace YOUR_API_KEY with the key from Settings → API Keys.
export CLOUDCLAWER_API_KEY="YOUR_API_KEY"
export API="https://api.cloudclawer.com"REGISTER=$(curl -sf -X POST "$API/u/dkr/apps/registry" \
-H "X-Api-Key: $CLOUDCLAWER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"manifest": {
"manifestVersion": "1",
"name": "my-server-app",
"displayName": "My Server App",
"description": "A minimal HTTP server app",
"version": "1.0.0",
"author": "your-username",
"entrypoint": "handler.mjs",
"execution": {
"lifecycle": "per-call",
"timeout_ms": 30000,
"input_schema": { "type": "object", "properties": {} }
},
"permissions": {},
"web": { "entrypoint": "server.mjs", "type": "server" }
}
}')
APP_ID=$(echo "$REGISTER" | python3 -c "import sys,json; print(json.load(sys.stdin)['appId'])")
HANDLER_URL=$(echo "$REGISTER" | python3 -c "import sys,json; print(json.load(sys.stdin)['uploadUrl'])")
echo "App ID: $APP_ID"The response includes an appId(save it — you'll use it in the URL later) and a presigned S3 URL for the tool handler.
Create a minimal handler.mjs — this is the tool-callable side of your app. For server apps it just needs to exist.
cat > handler.mjs << 'JS'
export default async function handler() { return { result: "ok" }; }
JS
curl -sf -X PUT "$HANDLER_URL" \
-H "Content-Type: text/javascript" \
--data-binary @handler.mjscurl -sf -X POST "$API/u/dkr/apps/registry/$APP_ID/activate" \
-H "X-Api-Key: $CLOUDCLAWER_API_KEY"
# → { "ok": true }WEB_UPLOAD=$(curl -sf -X POST "$API/u/dkr/apps/registry/$APP_ID/web/upload-url" \
-H "X-Api-Key: $CLOUDCLAWER_API_KEY")
WEB_URL=$(echo "$WEB_UPLOAD" | python3 -c "import sys,json; print(json.load(sys.stdin)['uploadUrl'])")Your bundle must be a zip with server.mjs at the root.
cat > server.mjs << 'JS'
export default async function handler(input) {
return {
status: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({ method: input.method, path: input.path }),
};
}
JS
zip bundle.zip server.mjs
curl -sf -X PUT "$WEB_URL" \
-H "Content-Type: application/zip" \
--data-binary @bundle.zipcurl -sf -X POST "$API/u/dkr/apps/registry/$APP_ID/web/activate" \
-H "X-Api-Key: $CLOUDCLAWER_API_KEY"
# → { "ok": true, "bundleType": "server" }Your app is now published. The stable URL is:
https://api.cloudclawer.com/apps/{appId{"}"}/web/Testing your app
The app URL requires no authentication. Any HTTP method is supported.
# GET request
curl "https://api.cloudclawer.com/apps/$APP_ID/web/"
# POST with a body
curl -X POST "https://api.cloudclawer.com/apps/$APP_ID/web/api/submit" \
-H "Content-Type: application/json" \
-d '{"name": "Alice"}'
# Sub-paths are forwarded as-is
curl "https://api.cloudclawer.com/apps/$APP_ID/web/dashboard/metrics?range=7d"server.mjs receives the path relative to /apps/{appId}/web. A request to /apps/{id}/web/api/submit arrives as input.path = "/api/submit".Error reference
| Status | Meaning | What to do |
|---|---|---|
503 | Publisher's sandbox is not running, or all 9 slots are full | The publisher must run cloudclawer launch |
502 | Launcher is running but the handler invocation failed | Check handler for crashes; re-deploy bundle |
504 | Handler took longer than 30 seconds | Optimize the handler or reduce workload |
404 | App not found or not yet published | Verify the appId and that web/activate was called |
Complete publish script
Copy-paste this to publish the echo server from scratch in one go:
#!/usr/bin/env bash
set -euo pipefail
API="https://api.cloudclawer.com"
KEY="$CLOUDCLAWER_API_KEY"
# Write the two handler files
cat > handler.mjs << 'JS'
export default async function handler() { return { result: "ok" }; }
JS
cat > server.mjs << 'JS'
export default async function handler(input) {
return {
status: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({ method: input.method, path: input.path, query: input.query }),
};
}
JS
# Register
REG=$(curl -sf -X POST "$API/u/dkr/apps/registry" \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d '{"manifest":{"manifestVersion":"1","name":"echo-server","displayName":"Echo Server",
"description":"Returns method and path of every request","version":"1.0.0",
"author":"your-username","entrypoint":"handler.mjs",
"execution":{"lifecycle":"per-call","timeout_ms":10000,"input_schema":{"type":"object","properties":{}}},
"permissions":{},"web":{"entrypoint":"server.mjs","type":"server"}}}')
APP_ID=$(echo "$REG" | python3 -c "import sys,json; print(json.load(sys.stdin)['appId'])")
H_URL=$(echo "$REG" | python3 -c "import sys,json; print(json.load(sys.stdin)['uploadUrl'])")
# Upload handler + activate
curl -sf -X PUT "$H_URL" -H "Content-Type: text/javascript" --data-binary @handler.mjs
curl -sf -X POST "$API/u/dkr/apps/registry/$APP_ID/activate" -H "X-Api-Key: $KEY"
# Get web upload URL, package zip, upload + activate
W_URL=$(curl -sf -X POST "$API/u/dkr/apps/registry/$APP_ID/web/upload-url" \
-H "X-Api-Key: $KEY" | python3 -c "import sys,json; print(json.load(sys.stdin)['uploadUrl'])")
zip bundle.zip server.mjs
curl -sf -X PUT "$W_URL" -H "Content-Type: application/zip" --data-binary @bundle.zip
curl -sf -X POST "$API/u/dkr/apps/registry/$APP_ID/web/activate" -H "X-Api-Key: $KEY"
echo ""
echo "Published! App ID: $APP_ID"
echo "Test: curl $API/apps/$APP_ID/web/"
echo " curl $API/apps/$APP_ID/web/hello?name=world"Frequently asked questions
Yes. The app runs inside the publisher's sandbox. If your sandbox stops (or times out), requests return 503. CloudClawer does not start a new container per request — the sandbox must already be running.
Add them to the zip alongside server.mjs: zip bundle.zip server.mjs utils.mjs data.json. Import them with relative paths inside server.mjs: import { helper } from "./utils.mjs".
Yes, using the standard fetch API. Outbound domains listed in permissions.proxy in your manifest are allowed. Add them before publishing.
Repeat steps 4–6: get a new presigned URL, upload the new zip, call web/activate again. The new bundle takes effect on the next request.
You can publish as many apps as you like. Each server app that is actively receiving traffic consumes one sandbox slot. You have 9 slots total, shared with custom tools.