super-productivity/packages/super-sync-server
Johannes Millan 85c7543f49 fix(supersync): dedupe WS connections by clientId to evict stale sockets
A reconnecting device kept appending alongside its stale entry instead of
replacing it, so userSet would fill to MAX_CONNECTIONS_PER_USER and reject
legitimate reconnects with 4008 until the 30s+10s heartbeat cycle caught up.
addConnection now evicts any existing entry with the same clientId before
adding the new socket; the cap still applies to genuinely distinct clientIds.
2026-05-13 19:48:43 +02:00
..
archive/encryption-attempts-openvz-incompatible chore(sync-server): archive non-working encryption implementations 2026-01-23 17:36:01 +01:00
docs feat(sync): add backup strategy with accounts-only dump and disaster recovery docs 2026-03-18 20:15:45 +01:00
helm/supersync revert(supersync): drop CLEANUP_INITIAL_DELAY_MS env var and 30-min prod default 2026-05-13 14:11:15 +02:00
prisma feat(supersync): persist full-state vector clock and add snapshot retry idempotency 2026-05-13 17:01:39 +02:00
public docs(super-sync): add 3-month inactive-account deletion clause 2026-04-25 22:36:14 +02:00
scripts fix(supersync): harden production server deploy 2026-05-13 02:10:46 +02:00
src fix(supersync): dedupe WS connections by clientId to evict stale sockets 2026-05-13 19:48:43 +02:00
tests fix(supersync): dedupe WS connections by clientId to evict stale sockets 2026-05-13 19:48:43 +02:00
tools chore(sync-server): archive non-working encryption implementations 2026-01-23 17:36:01 +01:00
.env.example revert(supersync): drop CLEANUP_INITIAL_DELAY_MS env var and 30-min prod default 2026-05-13 14:11:15 +02:00
.gitignore fix(sync): preserve own vector clock counter across full-state op resets 2026-04-01 15:41:13 +02:00
Caddyfile fix(sync-server): remove invalid timeout subdirective from Caddyfile 2026-03-23 13:05:40 +01:00
docker-compose.build.yml fix supersync deploy migrations 2026-05-12 15:39:26 +02:00
docker-compose.monitoring.yml feat(sync-server): add deployment and monitoring scripts 2025-12-19 14:26:12 +01:00
docker-compose.yml revert(supersync): drop CLEANUP_INITIAL_DELAY_MS env var and 30-min prod default 2026-05-13 14:11:15 +02:00
DOCKER-MONITORING.md Add active users monitoring command with engagement metrics (#6921) 2026-03-22 23:15:14 +01:00
Dockerfile fix supersync deploy migrations 2026-05-12 15:39:26 +02:00
Dockerfile.test refactor(sync): move vector clocks to sync-core 2026-05-11 15:21:08 +02:00
env.example revert(supersync): drop CLEANUP_INITIAL_DELAY_MS env var and 30-min prod default 2026-05-13 14:11:15 +02:00
package.json chore(deps): bump zod from 4.3.6 to 4.4.3 (#7431) 2026-05-09 18:47:34 +02:00
privacy-policy-en.md docs(compliance): document encryption risk and update privacy policy 2026-01-22 13:34:54 +01:00
privacy-policy.md docs(compliance): document encryption risk and update privacy policy 2026-01-22 13:34:54 +01:00
README.md fix(supersync): harden production server deploy 2026-05-13 02:10:46 +02:00
sync-server-architecture-diagrams.md refactor(sync): rename "stale" to "superseded" across sync/operation domain 2026-01-30 16:59:40 +01:00
terms-of-service-en.md docs(super-sync): add 3-month inactive-account deletion clause 2026-04-25 22:36:14 +02:00
terms-of-service.md docs(super-sync): add 3-month inactive-account deletion clause 2026-04-25 22:36:14 +02:00
tsconfig.json fix(sync-server): compile scripts for production Docker image 2025-12-19 15:58:21 +01:00
vitest.config.ts chore(sync): exclude integration test from default run, scope integration config 2026-03-17 13:59:40 +01:00
vitest.integration.config.ts chore(sync): exclude integration test from default run, scope integration config 2026-03-17 13:59:40 +01:00

SuperSync Server

A custom, high-performance synchronization server for Super Productivity.

Note: This server implements a custom operation-based synchronization protocol (Event Sourcing), not WebDAV. It is designed specifically for the Super Productivity client's efficient sync requirements.

Related Documentation:

Architecture

The server uses an Append-Only Log architecture backed by PostgreSQL (via Prisma):

  1. Operations: Clients upload atomic operations (Create, Update, Delete, Move).
  2. Sequence Numbers: The server assigns a strictly increasing server_seq to each operation.
  3. Synchronization: Clients request "all operations since sequence X".
  4. Snapshots: The server can regenerate the full state by replaying operations, optimizing initial syncs.

Key Design Principles

Principle Description
Server-Authoritative Server assigns monotonic sequence numbers for total ordering
Client-Side Conflict Resolution Server stores operations as-is; clients detect and resolve conflicts
E2E Encryption Support Payloads can be encrypted client-side; server treats them as opaque blobs
Idempotent Uploads Request ID deduplication prevents duplicate operations

Quick Start

The easiest way to run the server is using the provided Docker Compose configuration.

# 1. Copy environment example
cp env.example .env

# 2. Configure .env (Set JWT_SECRET, DOMAIN, POSTGRES_PASSWORD)
nano .env

# 3. Deploy the stack and run database migrations
./scripts/deploy.sh

docker compose up is not a deployment substitute: container startup migrations are disabled by default so app restarts cannot race the deploy migrator. ./scripts/deploy.sh runs prisma migrate deploy once before replacing the app container, then brings the stack up and verifies the health endpoint.

Leave DATABASE_URL unset when using the bundled Postgres service. The default connection uses postgres:5432; existing installs that already set DATABASE_URL with db:5432 keep working because the Compose service exposes db as a network alias.

Upgrade note: because RUN_MIGRATIONS_ON_STARTUP defaults to false, docker compose pull && docker compose up -d can leave the app running against unapplied migrations. Use ./scripts/deploy.sh for production updates, or ./scripts/deploy.sh --build for local image builds.

Some migrations use CREATE INDEX CONCURRENTLY, which can block on long-running transactions on a busy database. Run deploys off-hours when applying schema changes, and raise MIGRATION_TIMEOUT (seconds, default 900) if a large table requires more time. Exit code 124 from deploy.sh means the migration timed out — re-run after the blocking transaction clears.

If a deploy was interrupted after Prisma recorded the 20260512000000_add_full_state_sequence_index_drop_redundant_indexes migration as failed, later deploys can stop with P3009. Prisma can also stop this specific migration with P3018 because it contains several CREATE/DROP INDEX CONCURRENTLY statements, which cannot run in one transaction block. deploy.sh handles both cases: it resolves the failed row when needed, applies the concurrent index statements one at a time outside Prisma migrate, marks the migration applied, and retries migrate deploy.

If DATABASE_URL points to an external PostgreSQL server, set POSTGRES_SERVICE= to the empty value. deploy.sh then starts only the app/proxy services with compose dependencies disabled so the bundled Postgres container is not required. Prisma migrations still run against the configured DATABASE_URL.

Manual Setup (Development)

# Install dependencies
npm install

# Generate Prisma Client
npx prisma generate

# Set up .env
cp env.example .env
# Edit .env to point to your PostgreSQL instance (DATABASE_URL)

# Push schema to DB
npx prisma db push

# Start the server
npm run dev

# Or build and run
npm run build
npm start

Configuration

All configuration is done via environment variables.

Variable Default Description
PORT 1900 Server port
DATABASE_URL - PostgreSQL connection string (e.g. postgresql://user:pass@localhost:5432/db)
JWT_SECRET - Required. Secret for signing JWTs (min 32 chars)
PUBLIC_URL - Required. Public URL used for email links (e.g. https://sync.example.com)
CORS_ORIGINS https://app.super-productivity.com Allowed CORS origins
SMTP_HOST - SMTP Server for emails

API Endpoints

Authentication

Register a new user

POST /api/register
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "yourpassword"
}

Response:

{
  "message": "User registered. Please verify your email.",
  "id": 1,
  "email": "user@example.com"
}

Login

POST /api/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "yourpassword"
}

Response:

{
  "token": "jwt-token",
  "user": { "id": 1, "email": "user@example.com" }
}

Synchronization

All sync endpoints require Bearer authentication: Authorization: Bearer <jwt-token>

1. Upload Operations

Send new changes to the server.

POST /api/sync/ops

2. Download Operations

Get changes from other devices.

GET /api/sync/ops?sinceSeq=123

3. Get Snapshot

Get the full current state (optimized).

GET /api/sync/snapshot

4. Sync Status

Check pending operations and device status.

GET /api/sync/status

Client Configuration

In Super Productivity, configure the Custom Sync provider with:

  • Base URL: https://sync.your-domain.com (or your deployed URL)
  • Auth Token: JWT token from login

Maintenance

Scripts

The server includes scripts for administrative tasks. These use the configured database.

# Delete a user account
npm run delete-user -- user@example.com

# Clear sync data (preserves account)
npm run clear-data -- user@example.com

# Clear ALL sync data (dangerous)
npm run clear-data -- --all

API Details

Upload Operations (POST /api/sync/ops)

Request body:

{
  "ops": [
    {
      "id": "uuid-v7",
      "opType": "UPD",
      "entityType": "TASK",
      "entityId": "task-123",
      "payload": { "changes": { "title": "New title" } },
      "vectorClock": { "clientA": 5 },
      "timestamp": 1701234567890,
      "schemaVersion": 1
    }
  ],
  "clientId": "clientA",
  "lastKnownSeq": 100
}

Response:

{
  "results": [{ "opId": "uuid-v7", "accepted": true, "serverSeq": 101 }],
  "newOps": [],
  "latestSeq": 101
}

Download Operations (GET /api/sync/ops)

Query parameters:

  • sinceSeq (required): Server sequence number to start from
  • limit (optional): Max operations to return (default: 500)

Upload Snapshot (POST /api/sync/snapshot)

Used for full-state operations (BackupImport, SyncImport, Repair):

{
  "state": {
    /* Full AppDataComplete */
  },
  "clientId": "clientA",
  "reason": "initial",
  "vectorClock": { "clientA": 10 },
  "schemaVersion": 1
}

Security Features

Feature Implementation
Authentication JWT Bearer tokens in Authorization header
Timing Attack Mitigation Dummy hash comparison on invalid users
Input Validation Operation ID, entity ID, schema version validated
Rate Limiting Configurable per-user limits
Vector Clock Sanitization Limited to 50 entries, 255 char keys
Entity Type Allowlist Prevents injection of invalid entity types
Request Deduplication Prevents duplicate operations on retry

Multi-Instance Deployment Considerations

When deploying multiple server instances behind a load balancer, be aware of these limitations:

Passkey Challenge Storage

Issue: WebAuthn challenges are stored in an in-memory Map, which doesn't work across instances.

Symptom: Passkey registration/login fails if the challenge generation request hits instance A but verification hits instance B.

Solution for multi-instance:

  • Implement Redis-backed challenge storage
  • Or use sticky sessions (less ideal)

Current status: A warning is logged at startup in production if in-memory storage is used.

Snapshot Generation Locks

Issue: Concurrent snapshot generation prevention uses an in-memory Map.

Symptom: Same user may trigger duplicate snapshot computations across different instances.

Impact: Performance only (no data corruption) - snapshots are deterministic.

Solution for multi-instance:

  • Implement Redis distributed lock (optional, only for performance)

Single-Instance Deployment

For single-instance deployments, these limitations do not apply. The current implementation is fully functional and well-tested for single-instance use.

Security Notes

  • Set JWT_SECRET to a secure random value in production (min 32 characters).
  • Use HTTPS in production. The Docker setup includes Caddy to handle this automatically.
  • Restrict CORS origins in production.
  • Database backups are recommended for production deployments.