Skip to content
audreytPublic

About

Node.js port of Multi-user SocialCalc

Resources

Contributing

Stars

3.1k stars

Watchers

128 watching

Forks

Latest commit

 

History

1,783 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EtherCalc

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 for agent context and docs.ethercalc.net for architecture. The full rewrite plan lives in docs/historic/REWRITE_ULTRAPLAN.md.

Integrated with content management systems:

Browsers tested: Safari, Chrome, Firefox.

Install

Via npm (requires Bun ≥ 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.
ETHERCALC_AUTH (unset) Set 1 to enable passkey accounts/private rooms; requires the three trust anchors below.
ETHERCALC_RP_ID (unset) WebAuthn RP domain, e.g. sheets.example.com.
ETHERCALC_RP_NAME (unset) Passkey display name, e.g. EtherCalc.
ETHERCALC_ORIGIN (unset) Exact public HTTPS origin, e.g. https://sheets.example.com.

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.
  • To enable passkeys/private rooms, set ETHERCALC_AUTH=1, ETHERCALC_RP_ID, ETHERCALC_RP_NAME, and ETHERCALC_ORIGIN together. The RP ID and origin must match the HTTPS site users visit.

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 app directly (vp 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
vp exec wrangler deploy

Store the HMAC secret as a Worker secret:

vp exec 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-<timestamp>.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.<subdomain>.workers.dev
cd packages/worker
vp exec wrangler login       # one-time browser auth
vp exec wrangler deploy

# Mint a migration token and store it as a Cloudflare secret
TOKEN=$(openssl rand -hex 16)
echo "$TOKEN" | vp exec 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.<subdomain>.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

Install Vite+ and Bun 1.3.14. Vite+ is the repository interface; Bun remains the runtime for Bun-native scripts and tests.

vp install
vp build                              # browser assets + Worker build output
vp dev                                # browser assets + full Worker-backed app; URL printed in terminal

vp dev runs the real Hono + Durable Objects app, not a static client shell: open /:room for the single-sheet UI, /=:room for multi-sheet, and GET /_health to confirm readiness. For focused work on one package, the scoped commands still apply:

vp run @ethercalc/worker#dev          # wrangler dev --local, http://127.0.0.1:8787
vp run @ethercalc/worker#test         # workers-pool + node tests

Formal verification / Leanstral pump (optional; not required for app builds):

vp run verify:dafny      # LemmaScript → Dafny VCs (needs dafny on PATH)
vp run verify:lean       # LemmaScript → Lean gen + non-empty + fresh smoke
vp run verify:context    # needs sibling https://github.com/audreyt/socialcalc
vp 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. Full SocialCalc A1 algebra stays upstream in 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 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. 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.)

vex.js's own files were deleted (static/vex.css, static/vex-theme-flat-attack.css, static/vex.combined.min.js), but a pre-bundled copy of it still ships inlined inside static/ethercalc.js (confirmed via that bundle's own source map, which lists static/vex.combined.min.js among its sources - see docs/historic/REWRITE_ULTRAPLAN.md §7.4, a tracked, not-yet-audited legacy risk, not something this pass's scope covers).

  • static/ethercalc.js (inlined; vex.js portion only)

Third-party notices (npm-bundled, TypeScript rewrite)

Dependencies statically bundled into a shipped build artifact (where the published package's own license file wouldn't otherwise travel with the built output) are documented separately - third-party/m3e/NOTICE covers the Material 3 Expressive component bundle (packages/client/dist-passkey/ui.js): @m3e/web, @m3e/icons, Google's Material Symbols, and their own runtime dependencies.

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

About

Node.js port of Multi-user SocialCalc

Resources

Contributing

Stars

3.1k stars

Watchers

128 watching

Forks

Releases

Packages

Used by

Contributors

Languages