Founder Guide

Everything about how SandboxDrop works, from architecture to deployment.

01 Project Overview

SandboxDrop is a zero-signup ephemeral hosting platform for AI agents and developers. The core idea:

Target market: AI coding agents, developers needing quick previews, CI/CD preview environments, coding education.

Revenue model: Per-sandbox pricing in USDC. Static sites at $0.25 flat, compute sandboxes from $0.005/hr to $0.08/hr depending on resources.

02 Architecture

Client (curl / SDK / AI agent) | | POST /v1/deploy + payment v +--------------------------+ | API Server (Hono) | port 4000 | Bun + TypeScript | +--------------------------+ | | | v v v [PostgreSQL] [Docker] [x402 Facilitator] Drizzle ORM dockerode x402.org (testnet) | v +--------------------------+ | Sandbox Containers | ports 10000-60000 | oven/bun:1.1-alpine | +--------------------------+ ^ | +--------------------------+ | Proxy Server | port 4001 | sb-*.sandboxdrop.dev | routes by Host header +--------------------------+ ^ | End user browser Background: [Expiry Worker] — 30s interval, terminates expired sandboxes [Webhook Delivery] — fires events on lifecycle changes

Key design decisions

03 Tech Stack

ComponentTechnologyWhy
RuntimeBun 1.xFast startup, native TypeScript, built-in test runner, HTTP server
FrameworkHono 4.xLightweight, Bun-native, middleware system, Web Standards API
ORMDrizzle 0.38+Type-safe, no code generation needed, schema-as-code
DatabasePostgreSQL 15+JSONB for flexible data, reliable, excellent Drizzle support
ContainersDocker via dockerodeIndustry standard, good API client for Node/Bun
Paymentsx402 (@x402/hono)Coinbase's payment-as-auth protocol, gasless USDC on Base
ValidationZodTypeScript-first schema validation, integrates with Hono
IDsnanoidURL-safe, collision-resistant, short IDs with semantic prefixes

04 Container Provisioning

Every sandbox gets its own Docker container. The provisioning flow:

  1. API receives deploy request with files, runtime, and tier
  2. Docker container created from oven/bun:1.1-alpine base image
  3. Resource limits applied (CPU, memory, PIDs) based on tier
  4. Files injected via tar archive into /app directory
  5. Build command executed (if provided) via docker exec
  6. Container started with runtime-specific CMD
  7. Host port registered in proxy mapping

Runtime behavior

RuntimeHow it works
staticInjects a __server.js Bun.serve() static file server. User files go under /app/public/.
nodejs20Files injected at /app/. Default CMD: bun run /app/index.js.
python312Files injected at /app/. Requires start_cmd to specify entry point.
bunFiles injected at /app/. Default CMD: bun run /app/index.ts.

Security hardening

Docker security config (per container)
HostConfig: { NetworkMode: "bridge", // Bridge networking with port mapping PortBindings: { // Maps random host port → 3000 "3000/tcp": [{ HostPort: hostPort }], }, NanoCpus: Math.floor(cpus * 1e9), // CPU quota Memory: memoryMb * 1024 * 1024, // Hard memory limit MemorySwap: memoryMb * 1024 * 1024, // Swap = Memory (no extra swap) PidsLimit: 64, // Max 64 processes Tmpfs: { "/tmp": "rw,noexec,nosuid,size=512m" }, CapDrop: ["ALL"], // Drop ALL capabilities CapAdd: [ // Add back only essentials "NET_BIND_SERVICE", "CHOWN", "SETUID", "SETGID", "DAC_OVERRIDE" ], Privileged: false, ReadonlyRootfs: false, // Needed for file injection SecurityOpt: ["no-new-privileges"], }

Note: Bridge networking (with explicit port bindings) is used instead of host networking. Host networking doesn't work on Docker Desktop for macOS/Windows since containers run inside a VM. Each container listens on port 3000 internally, mapped to a random host port (10000–60000).

File injection

Files are injected into containers using a custom tar archive builder. The system builds a proper POSIX tar archive in memory:

Hot file updates (PATCH /sandbox/:id/files) use the same tar injection for new/updated files, and docker exec rm -f for deleted files.

05 Database Schema

6 tables managed by Drizzle ORM, plus 4 enums:

TablePurposeKey columns
sandboxesCore sandbox stateid (sb_), status, url, runtime, tier, container_id, container_port, cost_usdc, expires_at
sandbox_filesFile contents per sandboxsandbox_id, path, content
sandbox_logsSystem event logsandbox_id, level, message, timestamp
creditsPrepaid credit balancesid (cr_), token (sdt_live_), balance_usdc, refund_address
webhooksRegistered webhook URLscredit_token, url, events[]
newsletter_subscribersEmail signups from landing pageemail, subscribed_at

Enums

ID format

06 Payment Flow

x402 Protocol (Coinbase)

SandboxDrop uses x402 V2 for payment-as-auth on Base network (USDC). The flow:

  1. Client sends POST /v1/deploy without payment headers
  2. x402 middleware returns 402 Payment Required with PAYMENT-REQUIRED header
  3. Client wallet signs an ERC-3009 transferWithAuthorization (EIP-712 typed data)
  4. Client retries with PAYMENT-SIGNATURE header
  5. Facilitator verifies signature and settles payment (pays gas on behalf of buyer)
  6. Server returns resource with PAYMENT-RESPONSE header

Key detail: The x402 bypass in index.ts checks for Authorization: Bearer sdt_live_... headers. If present, the request skips x402 entirely and goes through the credit deduction path instead.

ConfigValue
NetworkBase Sepolia (testnet): eip155:84532
USDC contract0x036CbD53842c5426634e7929541eC2318f3dCF7e
Facilitatorhttps://x402.org (free testnet)
ProductionBase mainnet eip155:8453, Coinbase CDP facilitator

Credit system

Alternative to x402. User purchases credits via POST /credits/purchase, receives a sdt_live_ Bearer token.

07 API Routes

All routes are mounted under /v1 in a single Hono app. The middleware chain:

Middleware chain for POST /v1/deploy
cors()logger()x402 bypass checkoptionalAuthmanual JSON parse + Zod validateroute handler

Deploy flow (POST /deploy)

  1. Parse and validate JSON body with Zod schema
  2. Check credit balance (if Bearer token) or let x402 handle payment
  3. Deduct credits from balance
  4. Insert sandbox record in DB (status: provisioning)
  5. Store files in sandbox_files table
  6. Call createContainer() to provision Docker container
  7. Update DB with container_id and container_port (status: running)
  8. Register port mapping in reverse proxy
  9. Fire sandbox.ready webhook (non-blocking)
  10. Return sandbox details to client

On failure: marks sandbox as terminated, refunds credits, returns error detail.

08 Reverse Proxy

A separate Bun.serve() instance runs on port 4001 (configurable via PROXY_PORT). It routes sandbox URLs to containers.

  1. Extracts sandbox ID from Host header: sb-XXXX.sandboxdrop.devsb_XXXX
  2. Looks up host port in in-memory map (fast path)
  3. Falls back to DB lookup if not in memory
  4. Proxies request to http://127.0.0.1:{port}
  5. Adds X-Sandbox-Id header to response

Important: The API server (port 4000) and the proxy (port 4001) are separate servers. To access a sandbox via its URL locally, always use the proxy port:

Accessing sandboxes locally
# Via proxy (port 4001) — simulates production URL routing $ curl http://localhost:4001/ -H "Host: sb-XXXX.sandboxdrop.dev" # Or directly via the container's mapped host port (for debugging) $ curl http://localhost:{hostPort}/ # Port 4000 is the API — it will NOT proxy sandbox requests $ curl http://localhost:4000/v1/health # API endpoint

In production, this sits behind Caddy or Traefik for TLS termination and wildcard certificate handling.

09 Background Workers

Expiry worker

Runs every 30 seconds. Two jobs:

  1. Expire sandboxes: Finds sandboxes where expires_at < now and status != terminated. Terminates Docker container, updates DB, fires sandbox.terminated webhook.
  2. Expiry warnings: Finds running sandboxes expiring within 10 minutes. Fires sandbox.expiring webhook.

10 Webhook Delivery

When a lifecycle event occurs, the system:

  1. Looks up the sandbox's credit token
  2. Finds all webhooks registered to that credit token matching the event type
  3. Delivers payload via POST with Content-Type: application/json
  4. Retries up to 3 times with exponential backoff (1s, 2s, 4s delays)

Events

EventTrigger
sandbox.readyContainer provisioned and running
sandbox.expiring10 minutes before TTL expires
sandbox.terminatedSandbox stopped (expired, deleted, or failed)

11 Testing & CI

31 tests across 4 files, all passing:

FileTestsWhat it covers
pricing.test.ts10Cost calculation per runtime/tier, refund math, edge cases
ids.test.ts6ID generators: prefix format, length, uniqueness
api.test.ts11Integration tests: health, credits, newsletter, webhooks, sandbox lifecycle
container.test.ts4Module exports, proxy port mapping, webhook/expiry modules

CI Pipeline

GitHub Actions workflow (.github/workflows/ci.yml):

12 Development Setup

Getting started
# Prerequisites: Bun, PostgreSQL, Docker $ cd api && bun install $ export DATABASE_URL="postgresql://user:pass@localhost:5432/sandboxdrop" $ bun run db:push # Push schema to PostgreSQL $ bun run dev # Start with hot-reload (port 4000) # Run tests $ bun test # All 31 tests $ bun run test:unit # Pricing + ID tests only $ bun run test:integration # API integration tests

Environment variables

VariableRequiredDefaultDescription
DATABASE_URLYesPostgreSQL connection string
PORTNo4000API server port
PROXY_PORTNo4001Reverse proxy port
X402_RECEIVER_ADDRESSFor x4020x0...Wallet to receive USDC payments
X402_FACILITATOR_URLFor x402https://x402.orgx402 facilitator endpoint
X402_NETWORKFor x402eip155:84532CAIP-2 network ID

Debugging containers

Sandbox containers use labels prefixed with sandboxdrop. — use the full label key when filtering:

Docker debugging commands
# List all sandbox containers $ docker ps --filter "label=sandboxdrop.id" # Find a specific sandbox $ docker ps --filter "label=sandboxdrop.id=sb_XXXX" # Filter by name prefix (also works) $ docker ps --filter "name=sb-" # View container logs $ docker logs sb-XXXX # Check port mapping $ docker port sb-XXXX 3000/tcp -> 0.0.0.0:38421 # NOTE: "docker ps --filter label=sandboxdrop" won't match — # there is no label key "sandboxdrop", only "sandboxdrop.id" etc.

13 Domains & Infrastructure

DomainPurposeStatus
sandboxdrop.comLanding page + docsLive
api.sandboxdrop.dev/v1REST APIReady
sb-{id}.sandboxdrop.devSandbox URLs (via proxy)Ready
sb-{id}.ssh.sandboxdrop.dev:2222SSH accessPlanned
mcp.sandboxdrop.dev/sseMCP server for AI toolsPlanned

14 Feature Status

FeatureStatus
REST API (all 10 endpoints)Complete
PostgreSQL + Drizzle ORM schemaComplete
x402 payment middleware (Base Sepolia)Complete
Credit system (purchase, deduct, refund)Complete
Docker container provisioningComplete
Resource limits & security hardeningComplete
Reverse proxy (sandbox URL routing)Complete
Real-time log streaming (SSE)Complete
Hot file injection into containersComplete
TTL expiry worker (30s interval)Complete
Webhook delivery with retriesComplete
Test suite (31 tests)Complete
GitHub Actions CIComplete
Landing page with newsletter signupComplete
User docs page (/docs)Complete
SSH bastion accessPlanned
Dynamic x402 pricing per tierPlanned
MCP serverPlanned
SDK & CLI packages (npm/pypi)Planned
Production deployment (TLS, bridge networking)Planned
Content scanning & abuse preventionPlanned

15 Roadmap

Next priorities

  1. Production deployment — TLS via Caddy/Traefik, bridge networking with iptables, proper DNS wildcard
  2. SSH bastion — SSH access to running containers on port 2222 via bastion host
  3. MCP server — Enable AI tools (Claude Code, Cursor) to deploy directly
  4. Dynamic x402 pricing — Price x402 payments per tier instead of flat $0.25
  5. SDK packages — Publish sandboxdrop to npm and pypi
  6. Multipart deploy — Accept zip/tar.gz uploads alongside JSON file maps
  7. Content scanning — Automated abuse detection and content policy enforcement
  8. Dashboard — Optional web UI for credit token holders to manage sandboxes