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.jsonrequired. {"name": "Todo", "description": "..."}
public/index.htmlthe frontend (any HTML/CSS/JS). Required unless the app is API‑only.
public/**any other static assets.
api/<route>.jsone file per API route, an ES module.
api/_helpers.jsfiles 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
req.methodGET, POST, ...
req.pathpath inside the api, e.g. /todos/12
req.routefirst segment, selects the file: todos
req.subpaththe rest: /12
req.queryquery string as an object
req.headersa safe subset of request headers
req.bodybody as a string, or null
req.json()body parsed as JSON
req.bytes()body as a Buffer (uploads)
ctx
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 / deleteread as Buffer / string, list names, delete
ctx.user{email} of the signed‑in person, or null on a public app
ctx.envsecrets 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).

What a route returns
a stringHTML, 200
{json: value}JSON, 200
{status, headers, body}exactly that
a Uint8Array / Bufferraw bytes
nothing204

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). Use ctx.db, ctx.files and ctx.fetch. Safe built‑ins like crypto, path, url and buffer are 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 appId to update.

06Sharing & access

bob@example.comone person, after they sign in
domain:example.comanyone with an email at that domain
publicanyone with the link, no sign‑in

Roles are user (can open it) and editor (can also redeploy and manage sharing). The owner can additionally delete the app. Recipients sign in with an emailed, single‑use link that expires in 15 minutes.

07CLI reference

smallcloud serverun 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 listapps 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=valueset a secret; secrets rm to remove
smallcloud export <app> [file.zip]download source, database and files
smallcloud tokenslist agent tokens; tokens rm <id> to revoke
smallcloud contractprint the app contract
smallcloud mcp / mcp-installrun 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_URLthe public URL of the instance
SC_SECRET32 random bytes; encrypts secrets at rest. Back it up. Required in production.
RESEND_API_KEYmail provider for sign‑in links. Required in production.
SC_ALLOWED_EMAILSallowlist of who may sign in at all (recommended on private instances)
SC_APP_UID / SC_APP_GIDrun apps as a separate OS user (the kernel backstop; on by default in the Docker image)
SC_TRUST_PROXYset 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.

What is enforced
  • 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.fetch refuses 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.

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

Not defended in v1
  • Hostile deployers. --permission is a boundary, not a VM. Only let people you trust deploy; configure SC_APP_UID if 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.md for 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.