CloudClawer/CloudClawerDocs
BlogSign InSign In
FeaturesApps
Features

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

Tool app
A function your agent calls. Exposed as a tool in your sandbox.
handler.mjs
Static web app
Files served from S3. HTML, CSS, JS, images — no server needed.
index.html (in zip)
Server web app
A live HTTP handler. Any method, any path — routed through your sandbox.
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:

  1. Looks up the app's publisher username
  2. Claims a sandbox slot (port 8081–8089) from the publisher's running container
  3. Downloads bundle.zip via a presigned S3 URL and passes it to the launcher
  4. Calls the launcher's /invoke endpoint with the HTTP request as input
  5. Translates the handler's return value back into an HTTP response
The publisher must have a running sandbox for their apps to serve traffic. If no sandbox is running, or all 9 slots are occupied, callers receive 503. Server apps share the slot pool with custom tools (one slot each).

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

typescript
{
  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

typescript
{
  status:   number;                  // HTTP status code, e.g. 200
  headers?: Record<string, string>;  // optional response headers
  body:     string;                  // response body as a string
}

Minimal example

javascript
// 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)

javascript
// 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

javascript
// 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:

json
{
  "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

FieldTypeRequiredDescription
web.type"server" | "static"yesSet to "server" for a live HTTP handler
web.entrypointstringyesFor 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.

bash
export CLOUDCLAWER_API_KEY="YOUR_API_KEY"
export API="https://api.cloudclawer.com"
1
Register the app
bash
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.

2
Upload handler.mjs

Create a minimal handler.mjs — this is the tool-callable side of your app. For server apps it just needs to exist.

bash
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.mjs
3
Activate the app
bash
curl -sf -X POST "$API/u/dkr/apps/registry/$APP_ID/activate" \
  -H "X-Api-Key: $CLOUDCLAWER_API_KEY"
# → { "ok": true }
4
Get a presigned URL for the web bundle
bash
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'])")
5
Package and upload bundle.zip

Your bundle must be a zip with server.mjs at the root.

bash
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.zip
6
Activate the web bundle
bash
curl -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.

bash
# 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"
Your 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

StatusMeaningWhat to do
503Publisher's sandbox is not running, or all 9 slots are fullThe publisher must run cloudclawer launch
502Launcher is running but the handler invocation failedCheck handler for crashes; re-deploy bundle
504Handler took longer than 30 secondsOptimize the handler or reduce workload
404App not found or not yet publishedVerify the appId and that web/activate was called

Complete publish script

Copy-paste this to publish the echo server from scratch in one go:

bash
#!/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

Do I need to keep my sandbox running?

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.

Can my bundle include multiple files?

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".

Can my server app call external APIs?

Yes, using the standard fetch API. Outbound domains listed in permissions.proxy in your manifest are allowed. Add them before publishing.

How do I update the bundle?

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.

How many apps can I publish?

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.

Next steps

© NeuralAccel 2026