mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-22 07:28:14 +00:00
- Add accounts-only pg_dump (users + passkeys) to backup script for lightweight disaster recovery when clients still have data - Add pipefail to backup script to catch silent dump failures - Add test endpoint to simulate partial server revert (ops-after/:serverSeq) - Add 6 e2e tests covering all disaster recovery scenarios: complete data loss, partial revert, accounts-only restore (API + SQL), full dump restore + reset account, and all-clients-lost recovery - Add backup-and-recovery.md with setup, recovery procedures, and decision tree
301 lines
8.6 KiB
Markdown
301 lines
8.6 KiB
Markdown
# 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:**
|
|
>
|
|
> - [Authentication Architecture](./docs/authentication.md) - Auth design decisions and security features
|
|
> - [Operation Log Architecture](/docs/sync-and-op-log/operation-log-architecture.md) - Client-side architecture
|
|
> - [Server Architecture Diagrams](./sync-server-architecture-diagrams.md) - Visual diagrams
|
|
> - [Backup & Disaster Recovery](./docs/backup-and-recovery.md) - Backup setup and recovery procedures
|
|
|
|
## 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
|
|
|
|
### Docker (Recommended)
|
|
|
|
The easiest way to run the server is using the provided Docker Compose configuration.
|
|
|
|
```bash
|
|
# 1. Copy environment example
|
|
cp .env.example .env
|
|
|
|
# 2. Configure .env (Set JWT_SECRET, DOMAIN, POSTGRES_PASSWORD)
|
|
nano .env
|
|
|
|
# 3. Start the stack (Server + Postgres + Caddy)
|
|
docker-compose up -d
|
|
```
|
|
|
|
### Manual Setup (Development)
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```http
|
|
POST /api/register
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"email": "user@example.com",
|
|
"password": "yourpassword"
|
|
}
|
|
```
|
|
|
|
Response:
|
|
|
|
```json
|
|
{
|
|
"message": "User registered. Please verify your email.",
|
|
"id": 1,
|
|
"email": "user@example.com"
|
|
}
|
|
```
|
|
|
|
#### Login
|
|
|
|
```http
|
|
POST /api/login
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"email": "user@example.com",
|
|
"password": "yourpassword"
|
|
}
|
|
```
|
|
|
|
Response:
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```http
|
|
POST /api/sync/ops
|
|
```
|
|
|
|
#### 2. Download Operations
|
|
|
|
Get changes from other devices.
|
|
|
|
```http
|
|
GET /api/sync/ops?sinceSeq=123
|
|
```
|
|
|
|
#### 3. Get Snapshot
|
|
|
|
Get the full current state (optimized).
|
|
|
|
```http
|
|
GET /api/sync/snapshot
|
|
```
|
|
|
|
#### 4. Sync Status
|
|
|
|
Check pending operations and device status.
|
|
|
|
```http
|
|
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.
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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):
|
|
|
|
```json
|
|
{
|
|
"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.
|