# ethercalc **Repository Path**: chen_pei/ethercalc ## Basic Information - **Project Name**: ethercalc - **Description**: Node.js port of Multi-user SocialCalc - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-14 - **Last Updated**: 2026-07-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # EtherCalc * Overview: https://ethercalc.net/ * User guide: [docs.ethercalc.net](https://docs.ethercalc.net) (Starlight). Local: `bun run --cwd packages/docs dev` * 中文版: http://tw.ethercalc.net/ * 简体中文: http://cn.ethercalc.net/ * REST API: [API.md](./API.md) EtherCalc is a web spreadsheet for real-time collaborative editing. This branch is the TypeScript rewrite on the Cloudflare fullstack (Hono + Workers + Durable Objects + D1 + KV + R2). It deploys to Cloudflare via `wrangler deploy`, and **self-hosts anywhere via `docker compose up`** with no Cloudflare account required. See [AGENTS.md](./AGENTS.md) for agent context and [docs.ethercalc.net](https://docs.ethercalc.net) for architecture. The full rewrite plan lives in [docs/historic/REWRITE_ULTRAPLAN.md](./docs/historic/REWRITE_ULTRAPLAN.md). Integrated with content management systems: * [Drupal](https://drupal.org/project/sheetnode) Browsers tested: Safari, Chrome, Firefox. ## Install Via npm (requires [Bun](https://bun.sh/) ≥ 1.1 on PATH — the CLI spawns `bunx wrangler`): npm install -g ethercalc ethercalc # starts on http://localhost:8000 Via Docker (no Bun needed on the host — the image carries it): git clone https://github.com/audreyt/ethercalc cd ethercalc docker compose up -d The CLI path boots wrangler/Miniflare; the Docker path boots standalone workerd. Both need no Cloudflare account, and Docker persists room state under `./ethercalc-data/` (or `/data` in the container). ## Self-hosting ### Local / trusted LAN git clone https://github.com/audreyt/ethercalc cd ethercalc docker compose up -d This boots the standalone workerd Worker on `http://localhost:8000` and persists spreadsheet room state to `./ethercalc-data/` in the repo. No Redis, no Node runtime, no Cloudflare account. **Use this path only on a trusted network** (localhost, office LAN, VPN). It binds plaintext HTTP with no rate limiting or TLS. ### Internet-facing (required) If the instance is reachable from the public internet, **you must put a reverse proxy in front** that terminates TLS and applies rate limits. Plain `docker compose up` alone is not suitable for that threat model. The app deliberately keeps anonymous read/write for anyone who knows a room URL; the edge is where you bound request volume. A runnable nginx recipe ships in the repo: docker compose -f docker-compose.proxy.yml up -d The proxy config at `deploy/nginx/ethercalc.conf` sets a 25 MiB body limit to match the Worker write cap, limits request/connection rates per source address, and forwards WebSocket upgrades (with long read timeouts, so idle spreadsheets stay connected). For production HTTPS: place your certificates under `deploy/nginx/certs/`, uncomment the 443 listener in that file, **and** uncomment the 443 ports mapping in `docker-compose.proxy.yml` — or copy the same limits to your existing nginx/caddy/traefik edge. The bundled proxy serves the app at the URL root; don't combine it with `ETHERCALC_BASEPATH` (the config does no prefix stripping). ### Environment variables Override defaults by exporting these before `docker compose up`: | Variable | Default | Effect | | --------------------- | ----------- | ------------------------------------------------- | | `ETHERCALC_PORT` | `8000` | Listening port (remaps container bind). | | `ETHERCALC_HOST` | `0.0.0.0` | Listening address. | | `ETHERCALC_KEY` | *(unset)* | HMAC secret; enables read-only vs. edit auth. | | `ETHERCALC_DISABLE_ROOM_INDEX` | `1` | Hide `/_rooms*` and `/_exists/:room`. Set `0` to reopen (on the Docker image the directory endpoints then return empty bodies — there is no D1 index; only `/_exists` becomes a live oracle). | | `ETHERCALC_CORS` | *(unset)* | Legacy room-index gate; CORS headers are always permissive for embeds. | | `ETHERCALC_BASEPATH` | *(unset)* | URL prefix, e.g. `/ethercalc` behind a proxy. | | `ETHERCALC_EXPIRE` | *(unset)* | Seconds of inactivity before a room is pruned. | | `ETHERCALC_RATELIMIT` | *(unset)* | Optional in-Worker per-IP limit (off by default). `1` or `10` = 10 req/s; `60:600` = 600 per minute. Belt-and-suspenders behind nginx — not a substitute for the proxy. | | `ETHERCALC_ROOM_CREATE_LIMIT` | *(unset)* | Optional per-IP cap on room creation (`POST /_`, `/_new`, `/_from`, `PUT /_/room`). `1` = 6/min. Proxy compose defaults this on. | Recommended public-instance settings: - Set `ETHERCALC_KEY` if you want edit/delete URLs to require a per-room HMAC rather than anonymous write/delete. - Leave `ETHERCALC_DISABLE_ROOM_INDEX=1` unless you intentionally want a public room directory and existence oracle. - Set `ETHERCALC_EXPIRE` for public scratch instances, e.g. `ETHERCALC_EXPIRE=2592000` for a 30-day inactivity TTL. - **Always** use `docker-compose.proxy.yml` (or your own nginx/caddy/ traefik edge with equivalent limits) when the service is internet-facing. - Keep the container on plain HTTP and terminate TLS at the reverse proxy. If a local proxy fronts the container, publish the container port on loopback, e.g. `127.0.0.1:8000:8000`; do not change `ETHERCALC_HOST`, which must stay reachable inside the container. - Optionally set `ETHERCALC_RATELIMIT=1` for an extra in-Worker per-IP cap when nginx is already in place. On Apple Silicon, Docker Desktop's virtio networking has an intermittent quirk that can make `curl localhost:8000` hang even against a healthy container. If you hit it, run the worker directly (`bun run --cwd packages/worker dev`) or use a Linux host. ## CLI For non-Docker runs (local dev, systemd, etc.) use the `bin/ethercalc` wrapper. It accepts the legacy flag surface and forwards to `wrangler dev` + Miniflare/`--var` bindings: bin/ethercalc [--key SECRET] [--cors] [--port N] [--host ADDR] \ [--expire SEC] [--basepath PREFIX] \ [--persist-to DIR] Run `bin/ethercalc --help` for the full flag table. `--keyfile` / `--certfile` are accepted for backward compatibility but currently print a warning — `wrangler dev` does not expose TLS. Terminate TLS at a reverse proxy (nginx/caddy/traefik). The `ETHERCALC_*` environment variables from the table above work here too (exported before `bin/ethercalc`). Note that the CLI forwards them to `wrangler dev` as `--var` arguments, which are visible in the local process list — on shared machines, prefer a loopback bind or put secrets in `packages/worker/.dev.vars` instead of the environment. ## Deploy to Cloudflare cd packages/worker npx wrangler deploy Store the HMAC secret as a Worker secret: npx wrangler secret put ETHERCALC_KEY ## Staying on legacy (Redis-backed) EtherCalc `audreyt/ethercalc:latest` (and every `0.20260422.*` tag and later) ships the 2026 TypeScript rewrite. It stores rooms in Durable Object SQLite files, **not** Redis — pulling `latest` over an existing Redis-backed install will look broken until you migrate. To keep using Redis without migrating yet, pin the last pre-rewrite release: docker pull audreyt/ethercalc:0.20201228.1 Or use the bundled compose file (builds the same image locally if the tag is not cached yet): git clone https://github.com/audreyt/ethercalc cd ethercalc # Reuse your existing Redis data directory: ETHERCALC_LEGACY_REDIS_DATA=/var/lib/redis docker compose -f docker-compose.legacy.yml up -d Room state lives in Redis (`appendonly yes`). The legacy stack listens on port 8000 and speaks socket.io — same URLs and behaviour as pre-2026 self-hosts. When you are ready to move forward, see the migration section below. ## Migration from a legacy (Redis-backed) EtherCalc ### Turnkey (recommended) If you have a legacy Redis-backed EtherCalc and just want to upgrade: # Preserve the Redis dump outside the repo — this is your rollback point sudo cp /var/lib/redis/dump.rdb ~/ethercalc-dump-$(date +%F).rdb git clone https://github.com/audreyt/ethercalc cd ethercalc cp ~/ethercalc-dump-$(date +%F).rdb ./legacy-dump.rdb ./bin/migrate-legacy.sh One command stands up a temporary Redis loaded with your dump, builds and runs the new Worker, streams every room across, and writes a dated backup to `./backups/ethercalc-.tar.gz` containing both the migrated state and your source dump. On success the Worker is left running on http://localhost:8000 — open any existing room by its URL to confirm. Requires only `docker` + the `docker compose` plugin on the host. On Ubuntu: `sudo apt install -y docker.io docker-compose-plugin`. Tested against OrbStack and Docker Desktop on macOS/arm64; Docker Engine on Linux. ### Migrating rooms to a Cloudflare deployment Once the turnkey path above has verified locally, the same dump can be pushed to a Cloudflare Workers deployment. From the repo root: # Deploy the worker. Spits out https://ethercalc..workers.dev cd packages/worker npx wrangler login # one-time browser auth npx wrangler deploy # Mint a migration token and store it as a Cloudflare secret TOKEN=$(openssl rand -hex 16) echo "$TOKEN" | npx wrangler secret put ETHERCALC_MIGRATE_TOKEN # Stand up a temporary local Redis loaded with the legacy dump cd ../.. docker run -d --name ec-migrate-redis -p 6379:6379 \ -v "$PWD/legacy-dump.rdb:/input/dump.rdb:ro" \ redis:7-alpine sh -c \ 'cp /input/dump.rdb /data/dump.rdb && exec redis-server --save "" --appendonly no' sleep 3 # let redis finish loading the dump # Push every room up to the Cloudflare deployment ./bin/ethercalc migrate \ --source redis://localhost:6379 \ --target https://ethercalc..workers.dev \ --token "$TOKEN" docker rm -f ec-migrate-redis Then attach your domain in the Cloudflare dashboard under Workers & Pages → your worker → Triggers → Custom Domains. ### Manual (advanced) `bin/ethercalc migrate` streams a running Redis or Zedis directly into a Worker you already have up: bin/ethercalc migrate \ --source redis://localhost:6379 \ --target http://new-worker.example/ \ --token $ETHERCALC_MIGRATE_TOKEN O(1)-per-room memory regardless of dump size — Redis owns the decoding. The target endpoint is gated by `env.ETHERCALC_MIGRATE_TOKEN` (when unset, the route returns 404). Pass `--dry-run` to preview without writing. `--source file:///path` (or bare `/path`) also works for on-disk legacy dumps (the Sandstorm grain fallback format). ## Development bun install bun run --cwd packages/worker dev # wrangler dev --local bun run --cwd packages/worker test # workers-pool + node tests Formal verification / Leanstral pump (optional; not required for app builds): bun run verify:dafny # LemmaScript → Dafny VCs (needs dafny on PATH) bun run verify:lean # LemmaScript → Lean gen + non-empty + fresh smoke bun run verify:context # needs sibling https://github.com/audreyt/socialcalc bun run verify:request # concatenate prompt+context+Lean for Leanstral omp --print --no-tools --no-session --mode text \ --model mistral/labs-leanstral-1-5-1 @lemma/request.md Shipping TypeScript is the oracle. Dafny checks a reduced integer facade; Lean generation feeds Leanstral; only Bun tests decide production behavior. See [lemma/README.md](./lemma/README.md). Full SocialCalc A1 algebra stays upstream in [audreyt/socialcalc](https://github.com/audreyt/socialcalc) (`lemma/`). `verify:context` needs that repo checked out as `../socialcalc`; tracked `lemma/context.md` / `lemma/request.md` remain usable without regen. See [AGENTS.md](./AGENTS.md) for the directory map, testing strategy (100% line/branch/function/statement coverage plus Stryker mutation gates on gated packages), and the remaining phase plan. ## REST API See [API.md](./API.md). The public HTTP surface is preserved byte-for-byte where deterministic, minus a small allow-list of sensible fixes documented in AGENTS.md §6.1. # Licensing ### Common Public Attribution License (Socialtext Inc.) * socialcalcspreadsheetcontrol.js * socialcalctableeditor.js ### Artistic License 2.0 (Socialtext Inc.) * formatnumber2.js * formula1.js * socialcalc-3.js * socialcalcconstants.js * socialcalcpopup.js #### Artistic License 2.0 (Framasoft) * l10n/fr.json ### MIT License (John Resig, The Dojo Foundation) * static/jquery.js ### MIT License (HubSpot, Inc.) * static/vex-theme-flat-attack.css * static/vex.combined.min.js * static/vex.css ### MIT License (Stuart Knightley, David Duponchel, Franz Buchinger, Ant'onio Afonso) * static/jszip.js ### Apache License 2.0 (SheetJS) * static/shim.js * static/xlsx.core.min.js * static/xlsxworker.js * assets/start.html (xlsx2socialcalc.js) ### CC0 Public Domain (唐鳳 / Audrey Tang) * src/*.ls (legacy LiveScript sources, preserved until Phase 12 sweep) * packages/**/*.ts (TypeScript rewrite) ### Mozilla Public License Version 2.0 (LibreOffice contributors) * images/sc_*.png