Documentation
You hand smallcloud a folder. It gives you back a URL other people can open. The platform supplies hosting, a database, file storage, sign‑in, sharing and secrets.
01Quickstart
Run an instance, then deploy a folder to it.
# run the server git clone https://github.com/mandarwagh9/smallcloud && cd smallcloud npm install cp .env.example .env # then set SC_SECRET at least npm run dev # http://localhost:8787
# deploy a folder and share it npx smallcloud login http://localhost:8787 npx smallcloud deploy ./my-app # → http://localhost:8787/a/my-app npx smallcloud share my-app bob@example.com
With no RESEND_API_KEY set, sign‑in links are printed to the console and shown in the browser, so you can use it locally without a mail provider.
02What an app is
An app is a folder with a name, an optional frontend, and one file per API route.
| app.json | required. {"name": "Todo", "description": "..."} |
| public/index.html | the frontend (any HTML/CSS/JS). Required unless the app is API‑only. |
| public/** | any other static assets. |
| api/<route>.js | one file per API route, an ES module. |
| api/_helpers.js | files starting with _ are shared code, not routes. |
A request to /a/<slug>/api/todos runs api/todos.js. If there is no file for a route, api/index.js handles it (use it as a catch‑all router). Each path segment is one component: it is percent‑decoded, but a segment may not contain an encoded separator or .. (that request is rejected with 400). If an identifier can contain a slash, put it in the query string.
03Writing a route
export default async function (req, ctx) {
if (req.method === 'POST') {
const { text } = req.json();
ctx.db.run('insert into todos (text, done) values (?, 0)', text);
return { json: { ok: true } };
}
return { json: ctx.db.all('select * from todos order by id desc') };
}
| req.method | GET, POST, ... |
| req.path | path inside the api, e.g. /todos/12 |
| req.route | first segment, selects the file: todos |
| req.subpath | the rest: /12 |
| req.query | query string as an object |
| req.headers | a safe subset of request headers |
| req.body | body as a string, or null |
| req.json() | body parsed as JSON |
| req.bytes() | body as a Buffer (uploads) |
| ctx.db.run(sql, ...p) | write; returns {changes, lastInsertRowid} |
| ctx.db.get(sql, ...p) | one row, or undefined |
| ctx.db.all(sql, ...p) | array of rows |
| ctx.db.exec(sql) | schema statements; use create table if not exists at the top of every route |
| ctx.files.put(name, data) | store a file (string or Buffer) |
| ctx.files.get / getText / list / delete | read as Buffer / string, list names, delete |
| ctx.user | {email} of the signed‑in person, or null on a public app |
| ctx.env | secrets the owner set (never in your code) |
| ctx.log(...) | writes to the app log, visible via logs |
| ctx.fetch(url, init) | outbound HTTP; private/internal addresses are blocked |
ctx.user is the signed‑in person whenever there is one; it is null only on a public app opened by someone who never signed in. Don’t use it as a permission check for who may open the app — smallcloud already did that before your code ran. ctx.files is capped per app (default 100 MB and 10,000 files).
| a string | HTML, 200 |
| {json: value} | JSON, 200 |
| {status, headers, body} | exactly that |
| a Uint8Array / Buffer | raw bytes |
| nothing | 204 |
You may set content-type, content-disposition, cache-control, location, etag, last-modified, vary, link and any x-* header. Anything else is dropped and logged. An app cannot set set-cookie — apps share an origin with the platform, so keep per‑visitor state in ctx.db keyed by ctx.user.email.
04The frontend
public/index.html is served at the app root. Call your API with a relative path so the app works at any URL:
const res = await fetch('api/todos');You may load libraries from a CDN in the browser. There is no build step: ship plain HTML/CSS/JS or ES modules.
05Limits & rules
- Bundle: 5 MB, 500 files max.
- A request must finish in 10 seconds; the app gets 128 MB of heap.
api/code may not use npm packages in v1, and may not import these built‑ins:sqlite,fs,child_process,worker_threads,net,http,os,process,module,vm(a deploy that references one is rejected). Usectx.db,ctx.filesandctx.fetch. Safe built‑ins likecrypto,path,urlandbufferare fine.- An app can read only its own folder and write only its own data. It cannot start processes.
- Redeploying keeps the same URL, database and files. Deploy with the same
appIdto update.
07CLI reference
| smallcloud serve | run the server (reads .env / environment) |
| smallcloud login [url] | connect this machine to a server |
| smallcloud deploy <dir> [--app <id>] | deploy or update an app |
| smallcloud list | apps you own or that are shared with you |
| smallcloud open <app> | print an app’s URL |
| smallcloud share <app> <who> [role] | who = email | domain:example.com | public |
| smallcloud unshare <app> <who> | remove access |
| smallcloud logs <app> [--limit N] | read the app log |
| smallcloud db <app> "<sql>" | run SQL against the app’s database |
| smallcloud secrets set <app> KEY=value | set a secret; secrets rm to remove |
| smallcloud export <app> [file.zip] | download source, database and files |
| smallcloud tokens | list agent tokens; tokens rm <id> to revoke |
| smallcloud contract | print the app contract |
| smallcloud mcp / mcp-install | run the MCP server / register it with Claude Code |
08For agents (MCP)
smallcloud ships an MCP server, so a coding agent can deploy, share and debug without a human in the loop.
npx smallcloud login https://cloud.example.com
npx smallcloud mcp-install # registers with Claude Code
The agent then has smallcloud_deploy, smallcloud_share, smallcloud_logs, smallcloud_source, smallcloud_db, smallcloud_secret_set, smallcloud_export and the rest. The whole contract above is the one document it needs; it is also served at GET /v1/contract and printed by smallcloud contract.
09Running an instance
One container and one volume. Set at least SC_SECRET; set RESEND_API_KEY so magic links can be emailed in production.
export SC_BASE_URL=https://cloud.example.com export SC_SECRET=$(openssl rand -hex 32) export RESEND_API_KEY=... # for magic links docker compose up -d # includes Caddy for TLS
| SC_BASE_URL | the public URL of the instance |
| SC_SECRET | 32 random bytes; encrypts secrets at rest. Back it up. Required in production. |
| RESEND_API_KEY | mail provider for sign‑in links. Required in production. |
| SC_ALLOWED_EMAILS | allowlist of who may sign in at all (recommended on private instances) |
| SC_APP_UID / SC_APP_GID | run apps as a separate OS user (the kernel backstop; on by default in the Docker image) |
| SC_TRUST_PROXY | set to 1 behind a TLS proxy so rate limits see real client IPs |
See RUNBOOK.md in the repo for backups, upgrades and troubleshooting. Requires Node ≥ 22.15 (see the security model below).
10Security model
smallcloud runs code your agents write, on your server, for a handful of people you name. Each app runs in its own Node process started with --permission, granted access only to that app’s directory. App code is treated as hostile and confined; recipients are ordinary, untrusted web visitors; only people you give an account can deploy at all.
- An app cannot read files outside its own directory, read another app’s database, write outside its own data, spawn a process, or start a worker thread.
- Apps receive no platform environment variables.
ctx.fetchrefuses localhost, link‑local and private (RFC 1918) addresses across encodings, and re‑checks redirects. - A request over 10s is killed and the app recovers; an app crash never takes the control plane down.
- Secrets are AES‑256‑GCM at rest, decrypted only when handed to an app, never returned by the API. API tokens are stored only as a SHA‑256 hash and are revocable per token.
- Sign‑in is single‑use magic links (15 min) and 30‑day HttpOnly, SameSite=Lax session cookies; form posts are same‑origin checked.
Every one of these has a test in test/security.test.ts.
node:sqlite gap — read this before opening an instance.
Node’s permission model does not cover the native file access inside node:sqlite: with --permission active, fs.readFileSync of the platform DB is denied, but opening it through node:sqlite is not. Three layers close it — a deploy‑time guardrail (always), a load‑time import block via module.registerHooks (Node ≥ 22.15), and OS user separation via SC_APP_UID (the kernel backstop, on by default in the Docker image). On Node < 22.15 with no SC_APP_UID, treat anyone who can deploy as having read access to the platform database.
Recommended production configuration: Node ≥ 22.15 (the bundled Docker image) and SC_APP_UID/SC_APP_GID pointing at a user that cannot read platform.db. That combination does not depend on the permission model covering any particular builtin.
- Hostile deployers.
--permissionis a boundary, not a VM. Only let people you trust deploy; configureSC_APP_UIDif that assumption weakens. - Cross‑app browser isolation. All apps share one origin (
/a/<slug>), so treat apps on one instance as mutually trusting in the browser until per‑app subdomains land. - DNS rebinding, raw sockets on old Node, side channels, and co‑tenant CPU denial‑of‑service are documented, not fully mitigated. See
SECURITY.mdfor the complete list.
11Exporting & leaving
npx smallcloud export my-app
A zip with your source, your SQLite database and your uploaded files. Nothing in an app is smallcloud‑specific except the ctx object, which is about forty lines to reimplement. The export endpoint builds the zip in memory and is capped at 200 MB per app; a larger app leaves via scripts/backup.sh, which streams to tar.