Sandcastle Documentation
Everything you need to go from “AI wrote some code” to a live website on your own server.
What is Sandcastle?
Sandcastle is a small program you install on a Linux server or home device. Once it's running, you can take any code — a website, a small app, a tool — and turn it into a real, working website with its own address like my-app.yourname.com.
It's designed for people who use AI tools (ChatGPT, Claude, Cursor, etc.) to generate code and want a simple way to put that code online. You don't need to understand programming or servers — Sandcastle figures out what the AI made and handles the rest.
What it does:
- Takes code (from AI or anywhere) and makes it a live website
- Auto-detects the programming language and framework
- Gives you a secure HTTPS link you can share
- Runs everything in isolated containers so bad code can't harm your server
- Works with Claude, ChatGPT, Cursor, and any other AI tool
Three parts:
- Server — runs on your Linux machine, manages deployments and containers
- CLI — command-line tool you run on your laptop to deploy code
- MCP — lets AI tools (Claude, Cursor) deploy code for you automatically
What you need
You need two things to get started:
- A Linux server or computer — Ubuntu 22.04 or 24.04 (x86_64). A cheap VPS works great — a Hetzner CX22 (~$4/month) is more than enough. A home server, old laptop, or Raspberry Pi (with x86) also works.
- A domain name you control — something like
sandcastle.example.com. You can buy one from Namecheap, Cloudflare, or any registrar.
DNS setup
Point your domain and a wildcard subdomain at your server's IP address. Go to your domain registrar's DNS settings and add two A records:
A sandcastle.example.com → your-server-ip A *.sandcastle.example.com → your-server-ip
Setting up the server
The installer sets up everything on your server over SSH — Docker, gVisor (the security sandbox), Caddy (for HTTPS), and the Sandcastle server itself.
Step 1: Build the installer and server
On your local machine (the one you're working from):
make installer make deploy
Step 2: Run the installer
sandcastle-installer ssh \ --host 1.2.3.4 \ --domain sandcastle.example.com \ --binary ./bin/sandcastle-server-linux
Installer flags
| Flag | Default | Description |
|---|---|---|
--host | — | Server IP address (required) |
--domain | — | Your domain (required) |
--binary | — | Path to server binary (required) |
--user | root | SSH user |
--key | — | SSH private key path |
--password | — | SSH password (if no key) |
--port | 22 | SSH port |
--admin-key | auto-generated | Admin API key |
--max-deploys | 10 | Max deployments per user |
--default-ttl | 72 | Default TTL in hours |
What the installer does (step by step)
- Verifies root access
- Installs system packages (ca-certificates, curl, jq, sqlite3, dnsutils)
- Installs Docker CE
- Installs gVisor (runsc) and configures Docker to use it
- Tests gVisor runtime
- Installs Caddy (for automatic HTTPS)
- Pulls base Docker images (node:20-slim, caddy:2-alpine)
- Creates data directories (
/var/lib/sandcastle/) - Creates shared Docker volumes (npm cache)
- Writes server config (
/var/lib/sandcastle/config.json) - Initializes SQLite database with admin user
- Writes Caddyfile with Let's Encrypt config
- Uploads server binary to
/usr/local/bin/sandcastle-server - Writes
sandcastle-adminhelper script - Writes systemd service unit
- Enables and starts Caddy + Sandcastle services
- Configures UFW firewall (ports 22, 80, 443)
- Verifies DNS resolution
- Runs health check
- Prints summary with your admin API key
Installing the CLI
The CLI is the command you run on your laptop to deploy code. Build it from source:
make cli sudo mv bin/sandcastle /usr/local/bin/
Then tell it where your server is:
sandcastle config set server https://sandcastle.example.com sandcastle config set key sc_your_admin_key_here
You can also log in via the browser instead:
sandcastle login
Your first deploy
Once the CLI is configured, navigate to a project directory. First, set up your project:
$ sandcastle init 🏰 Sandcastle Project Setup Detected: Next.js ✓ Saved .sandcastle.json
This walks you through naming your deployment, setting a TTL, memory limit, and access protection. Then deploy:
$ sandcastle deploy Uploading 47 files Building... ✓ Live at https://my-app.sandcastle.example.com
That's it. Open the link and your app is running. You can skip init and go straight to deploy — it works fine with defaults.
You can also deploy a single file directly:
sandcastle deploy App.jsx # Single React file sandcastle deploy index.html # Static HTML file sandcastle deploy app.py # Python file
Single .jsx, .tsx, .vue, .svelte, and .html files are auto-scaffolded with Vite + Tailwind CSS.
Use cases
People use Sandcastle for all kinds of things:
- Family tools — meal planners, chore trackers, shopping lists, recipe collections
- School projects — quizzes, class websites, science fair dashboards
- Small business apps — inventory trackers, appointment booking, invoicing tools
- Weekend experiments — trying out an AI-generated idea to see if it works
- Internal tools — dashboards, admin panels, status pages
- RSVP pages — quick event pages for parties, weddings, meetups
- Prototypes — show a working demo to a client or friend
The common pattern: tell an AI tool what you want, it writes the code, and Sandcastle makes it a real website you can share.
Sharing & access control
By default, every deployment is public — anyone with the link can see it. You can add protection:
Password protection (basic auth)
sandcastle protect abc123 --mode basic_auth --username admin --password secret
Visitors see a browser login prompt.
API key protection
sandcastle protect abc123 --mode api_key
Access requires an API key in the header or query string.
User-based protection
sandcastle protect abc123 --mode user
Only logged-in Sandcastle users can access the deployment. Optionally restrict to specific users:
sandcastle protect abc123 --mode user --users alice,bob
The deployment owner and admins always have access regardless of the user list.
Remove protection
sandcastle protect abc123 --mode public
CLI Reference
sandcastle <command> [options]
Commands
| Command | Description |
|---|---|
init | Configure project for Sandcastle (creates .sandcastle.json) |
deploy [file|dir] | Deploy a file, directory, or current directory |
dev [dir] | Start sandbox with live file sync (hot reload) |
update <id> | Push code update to a running deployment |
promote <id> | Promote a sandbox to a production deployment |
list | List your deployments |
status <id> | Get deployment details |
logs <id> | View container logs |
protect <id> | Set access control (basic_auth, api_key, user, public) |
pull <id> | Download deployment source code to local directory |
destroy <id> | Remove a deployment |
destroy --all | Remove all your deployments |
login | Login via browser (sets up API key automatically) |
config | Manage CLI configuration |
Init options
| Flag | Default | Description |
|---|---|---|
--yes | — | Accept all defaults (non-interactive) |
--dir <path> | current dir | Project directory |
Running sandcastle init creates a .sandcastle.json file in your project with deployment name, TTL, memory limit, port, and access protection settings. These are used automatically by sandcastle deploy and sandcastle dev.
Deploy options
| Flag | Default | Description |
|---|---|---|
--name <name> | directory name | Custom subdomain name |
--ttl <duration> | 72h | Time to live: 24h, 7d, 0 for never |
--env <KEY=VAL> | — | Environment variable (repeatable) |
--memory <limit> | 512m | Memory limit |
--cpus <limit> | 0.5 | CPU limit |
--dir <path> | current dir | Project directory |
--port <port> | auto-detect | Port your app listens on |
--env-file <path> | — | Load env vars from file (also auto-loads .env) |
--protect <mode> | — | Access control: basic_auth, api_key, user |
--users <user,...> | — | Restrict access to specific users (for user mode) |
--network-quota <q> | — | Network transfer quota (e.g. 1g, 5g, 10g) |
Dev options
| Flag | Default | Description |
|---|---|---|
--attach <id> | — | Reattach file watcher to existing sandbox |
--name <name> | directory name | Custom subdomain name |
--ttl <duration> | 4h | Time to live |
--env <KEY=VAL> | — | Environment variable (repeatable) |
--memory <limit> | 1g | Memory limit |
--port <port> | auto-detect | Port override |
--protect <mode> | — | Access control: basic_auth, api_key, user |
--users <user,...> | — | Restrict access to specific users (for user mode) |
--network-quota <q> | — | Network transfer quota (e.g. 500m, 1g) |
Examples
sandcastle init # Configure project interactively sandcastle init --yes # Non-interactive with defaults sandcastle deploy # Deploy current directory sandcastle deploy App.jsx # Single-file React deploy sandcastle dev # Dev sandbox with file watcher sandcastle dev App.jsx # Single-file dev with hot reload sandcastle dev --attach abc123 # Reattach to existing sandbox sandcastle deploy App.jsx --name cool # Custom subdomain sandcastle update abc123def # Push code update sandcastle pull abc123def # Download deployment source sandcastle pull abc123def --output ./bak # Download to specific directory sandcastle logs abc123def --tail 50 # View last 50 log lines sandcastle config set server https://sandcastle.example.com sandcastle config set key sc_your_api_key_here
MCP Setup
MCP (Model Context Protocol) lets AI tools deploy code automatically. Sandcastle has two MCP implementations:
- Standalone MCP (stdio) — separate binary for Claude Desktop, Cursor, and other local MCP clients
- Server-side MCP (Streamable HTTP) — built into the server, used by Claude.ai web interface via OAuth
Claude Desktop
Build the MCP binary, then add it to your Claude Desktop config:
make mcp
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"sandcastle": {
"command": "/usr/local/bin/sandcastle-mcp",
"env": {
"SANDCASTLE_SERVER": "https://sandcastle.example.com",
"SANDCASTLE_KEY": "sc_your_api_key_here"
}
}
}
}
Cursor
Add to .cursor/mcp.json in your project or global config:
{
"mcpServers": {
"sandcastle": {
"command": "/usr/local/bin/sandcastle-mcp",
"env": {
"SANDCASTLE_SERVER": "https://sandcastle.example.com",
"SANDCASTLE_KEY": "sc_your_api_key_here"
}
}
}
}
Claude.ai (server-side MCP)
The server exposes an MCP endpoint at /mcp using Streamable HTTP transport with OAuth 2.1 authentication. Claude.ai can connect directly — no separate binary needed. Point Claude.ai at https://sandcastle.example.com/mcp.
MCP tools
Both MCP implementations expose these tools. The standalone MCP also adds sandcastle_deploy_dir and sandcastle_update_dir for deploying local directories.
| Tool | Description |
|---|---|
sandcastle_deploy | Deploy inline files to a live HTTPS URL |
sandcastle_deploy_dir | Deploy a local directory (standalone only) |
sandcastle_update | Update a deployment with new inline files |
sandcastle_update_dir | Update from a local directory (standalone only) |
sandcastle_status | Check deployment status and URL |
sandcastle_logs | Get container logs |
sandcastle_list | List all active deployments |
sandcastle_source | Get the source code files of a deployment |
sandcastle_destroy | Destroy a deployment |
sandcastle_protect | Set access control on a deployment |
sandcastle_sandbox_create | Create a dev sandbox with hot reload |
sandcastle_sandbox_push | Push file changes to a sandbox (instant) |
sandcastle_sandbox_install | Install/remove packages in a sandbox |
sandcastle_sandbox_exec | Run a shell command in a sandbox |
sandcastle_sandbox_logs | Get sandbox logs |
sandcastle_sandbox_destroy | Destroy a sandbox |
sandcastle_sandbox_promote | Promote a sandbox to production |
sandcastle_update_quota | Update network transfer quota |
Supported Languages
Sandcastle auto-detects the language and framework from your project files. Detection order: Go → Rust → Python → Node.js → Static HTML (first match wins).
Node.js
Detection: package.json exists with a start script or a recognized framework dependency.
Default version: Node 20 (override with .nvmrc, .node-version, or engines.node in package.json).
Recognized frameworks:
| Framework | Detection | Default Port |
|---|---|---|
| Next.js | next in dependencies | 3000 |
| Nuxt | nuxt in dependencies | 3000 |
| SvelteKit | @sveltejs/kit in dependencies | 3000 |
| Vite | vite in dependencies | 4173 |
| Create React App | react-scripts in dependencies | 3000 |
| Vue CLI | @vue/cli-service in dependencies | 8080 |
| Angular | @angular/cli or @angular/core in dependencies | 4200 |
| Generic Node | start script in package.json | 3000 |
Package managers: npm, yarn, and pnpm are supported. Sandcastle detects the lock file and uses the correct installer.
Python
Detection: Any of these files: requirements.txt, pyproject.toml, Pipfile, setup.py, setup.cfg, app.py, main.py, manage.py.
Default version: Python 3.12 (override with .python-version, requires-python in pyproject.toml, or runtime.txt).
Recognized frameworks:
| Framework | Detection | Default Port |
|---|---|---|
| Django | manage.py exists | 8000 |
| FastAPI | fastapi in requirements/pyproject | 8000 |
| Flask | flask in requirements/pyproject | 8000 |
| Generic Python | app.py or main.py exists | 8000 |
Go
Detection: go.mod exists.
Default version: Go 1.22 (reads the go directive from go.mod).
Build: CGO_ENABLED=0 go build produces a static binary. Runtime uses a minimal debian:bookworm-slim image.
Default port: 8080. Use the PORT environment variable in your code.
Rust
Detection: Cargo.toml exists.
Default version: latest stable (override with rust-toolchain.toml, rust-toolchain, or rust-version in Cargo.toml).
Build: cargo build --release. Runtime uses a minimal debian:bookworm-slim image.
Default port: 8080. Use the PORT environment variable in your code.
Static HTML
Detection: index.html exists (and no other language matched first).
Served directly by Caddy (Alpine). No build step. Supports the /_data/* REST API for storing data without a backend.
Sandbox / Dev Mode
Sandboxes are live development environments with hot reload. Changes apply instantly without a full rebuild — much faster for iterating.
CLI: sandcastle dev
sandcastle dev # Start dev sandbox with file watcher sandcastle dev App.jsx # Single-file dev with hot reload sandcastle dev --attach id # Reattach to existing sandbox
The CLI watches your local files and pushes changes to the sandbox as you edit. For Node/Vite projects, changes appear via HMR (Hot Module Replacement). For Go and Rust, the sandbox automatically rebuilds the binary.
MCP: sandbox tools
AI tools use these MCP tools for the same workflow:
sandcastle_sandbox_create— create the sandbox with initial filessandcastle_sandbox_push— push file changes (instant, no rebuild)sandcastle_sandbox_install— add or remove packagessandcastle_sandbox_exec— run commands inside the containersandcastle_sandbox_logs— check logs for errorssandcastle_sandbox_promote— ship it to production when ready
HTTP API
| Endpoint | Description |
|---|---|
POST /api/sandbox | Create a sandbox (JSON body with files) |
POST /api/sandbox/{id}/push | Push file changes |
POST /api/sandbox/{id}/install | Install packages |
POST /api/sandbox/{id}/exec | Run a command |
GET /api/sandbox/{id}/logs | Get logs |
DELETE /api/sandbox/{id} | Destroy sandbox |
POST /api/sandbox/{id}/promote | Promote to production |
Promote to production
When you're happy with your sandbox, promote it. This stops the dev server, runs a full optimized build, and starts a production runtime. The URL stays the same.
sandcastle promote abc123
Persistent Data
Deployments get two ways to persist data across rebuilds:
1. /data directory (backend apps)
Every dynamic container (Node.js, Python, Go, Rust) has a persistent /data directory. Use the DATA_DIR environment variable to reference it.
const path = require('path'); const dbPath = path.join(process.env.DATA_DIR, 'app.db');
import os db_path = os.path.join(os.environ["DATA_DIR"], "app.db")
This directory survives code updates and rebuilds. It's deleted when the deployment is destroyed.
2. /_data/* REST API (any deployment)
A built-in document store accessible via fetch() from any deployed page. No backend code needed — works with static HTML.
All requests go to /_data/ on the deployment's own domain:
// Create a document await fetch('/_data/votes', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({option: 'pizza'}) }); // List documents const {documents} = await fetch('/_data/votes').then(r => r.json()); // Get a document const doc = await fetch('/_data/votes/cs1abc').then(r => r.json()); // Update a document await fetch('/_data/votes/cs1abc', { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({option: 'pizza', count: 2}) }); // Delete a document await fetch('/_data/votes/cs1abc', {method: 'DELETE'});
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /_data | List collections |
POST | /_data/{collection} | Create a document |
GET | /_data/{collection} | List documents (?limit=10&offset=20) |
GET | /_data/{collection}/{id} | Get a document |
PUT | /_data/{collection}/{id} | Update a document |
DELETE | /_data/{collection}/{id} | Delete a document |
DELETE | /_data/{collection} | Delete entire collection |
Limits
| Limit | Value |
|---|---|
| Max document size | 100 KB |
| Max documents per collection | 10,000 |
| Max collections per deployment | 100 |
| Max total storage | 50 MB |
Server Configuration
The server reads configuration from /var/lib/sandcastle/config.json. The installer creates this file for you.
{
"domain": "sandcastle.example.com",
"port": 3456,
"dataDir": "/var/lib/sandcastle",
"defaultTTLHours": 72,
"defaultMaxDeploys": 10,
"containerDefaults": {
"memory": "512m",
"cpus": "0.5",
"pidsLimit": 256,
"diskSize": "2g",
"networkQuota": "5g"
}
}
Configuration fields
| Field | Default | Description |
|---|---|---|
domain | localhost | Your domain name |
port | 3456 | Port the server listens on (behind Caddy) |
dataDir | /var/lib/sandcastle | Data directory (DB, deploys, uploads) |
defaultTTLHours | 72 | Default deployment lifetime in hours |
defaultMaxDeploys | 10 | Max deployments per user |
containerDefaults.memory | 512m | Default memory limit per container |
containerDefaults.cpus | 0.5 | Default CPU limit per container |
containerDefaults.pidsLimit | 256 | Max processes per container |
containerDefaults.diskSize | — | Container root filesystem limit (requires overlay2+xfs+pquota) |
containerDefaults.networkQuota | — | Max network transfer per container (requires iptables quota module) |
Environment variable
DATA_DIR — overrides the data directory. Defaults to /var/lib/sandcastle.
File paths on server
| Path | Description |
|---|---|
/var/lib/sandcastle/config.json | Server configuration |
/var/lib/sandcastle/sandcastle.db | SQLite database (WAL mode) |
/var/lib/sandcastle/deploys/{id}/ | Deployment source code |
/var/lib/sandcastle/persist/{id}/ | Persistent data (/data directory) |
/etc/caddy/sites/{subdomain}.caddy | Caddy reverse proxy configs |
/etc/systemd/system/sandcastle.service | Systemd service unit |
/usr/local/bin/sandcastle-server | Server binary |
API Reference
All API endpoints require authentication via Authorization: Bearer <api_key> header. The server listens on 127.0.0.1:3456 behind Caddy.
Deployments
/api/deployUpload a tarball to deploy. Multipart form with tarball field.
/api/deploy/codeDeploy inline files as JSON. Body: {"files": {"index.html": "..."}, "name": "my-app"}
/api/deploy/{id}/updateUpdate a deployment with a new tarball.
/api/deploy/{id}/update/codeUpdate a deployment with inline files.
/api/deploymentsList all deployments for the authenticated user.
/api/deploymentsDestroy all deployments for the authenticated user.
/api/deployments/{id}Get deployment status, URL, framework, and details.
/api/deployments/{id}Update deployment metadata (e.g., network quota).
/api/deployments/{id}Destroy a deployment permanently.
/api/deployments/{id}/logsGet container logs. Optional ?tail=N query parameter.
/api/deployments/{id}/sourceDownload deployment source code. Optional ?format=json for JSON response with file contents.
/api/deployments/{id}/protectSet access control. Body: {"mode": "basic_auth", "username": "...", "password": "..."} or {"mode": "user", "users": ["alice", "bob"]}
Sandbox
/api/sandboxCreate a dev sandbox. Body: {"files": {...}, "name": "...", "env": {...}}
/api/sandbox/{id}/pushPush file changes. Body: {"changes": [{"op": "write", "path": "app.js", "content": "..."}]}
/api/sandbox/{id}/installInstall packages. Body: {"packages": ["express", "cors"]}
/api/sandbox/{id}/execExecute a command. Body: {"command": "ls -la", "timeout": 30}
/api/sandbox/{id}/logsGet sandbox logs. Optional ?tail=N.
/api/sandbox/{id}Destroy a sandbox.
/api/sandbox/{id}/promotePromote to production deployment.
Data API
Per-deployment document store. See Persistent Data for details.
/_data/_data/{collection}/_data/{collection}/_data/{collection}/{id}/_data/{collection}/{id}/_data/{collection}/{id}/_data/{collection}Gallery
/api/galleryPublic gallery listing of deployments.
Admin
/api/admin/usersList all users (admin only).
/api/admin/usersCreate a user.
/api/admin/users/{name}Delete a user.
/api/admin/users/{name}Update user settings.
/api/admin/settingsGet server settings.
/api/admin/settingsUpdate server settings.
OAuth 2.1
/.well-known/oauth-protected-resource/.well-known/oauth-authorization-server/oauth/register/oauth/authorize/oauth/tokenMCP
/mcpMCP Streamable HTTP endpoint (JSON-RPC 2.0). Used by Claude.ai web interface.
Security
Sandcastle assumes all deployed code is potentially hostile and sandboxes everything:
Container isolation
- gVisor (runsc) — every container runs under gVisor, a user-space kernel that intercepts system calls. Even if code exploits a vulnerability, it can't reach the host.
- All capabilities dropped — containers have no Linux capabilities (no
CAP_NET_RAW, noCAP_SYS_ADMIN, nothing). - No host access — containers can't see host filesystems, processes, or network interfaces.
- Localhost-only ports — containers bind to
127.0.0.1:port. Caddy handles all external traffic and TLS termination.
Resource limits
| Resource | Default Limit |
|---|---|
| Memory | 512 MB (runtime), 1 GB (sandbox) |
| CPU | 0.5 cores |
| PIDs | 256 processes |
| Disk | Configurable via diskSize |
| Network transfer | Configurable via networkQuota (iptables) |
HTTPS
All traffic is served over HTTPS. Caddy automatically provisions and renews TLS certificates from Let's Encrypt using HTTP-01 challenges. No manual certificate management needed.
Hard delete
Destroying a deployment removes everything — the database record, source code, Docker volumes, persistent data, and Caddy proxy config. Nothing is left behind.
Two-phase deployment
- Build phase — unconstrained memory, gVisor sandboxed, produces artifacts in a Docker volume
- Runtime phase — tight resource limits (512 MB, 0.5 CPU default), gVisor sandboxed, serves the pre-built output
Blue-green updates
When you update a deployment, a new volume is created and the build runs while the old container continues serving. On success, traffic swaps to the new container. On failure, the old container stays running (automatic rollback).