super-productivity/packages/super-sync-server/helm/supersync/values.yaml
Thorsten Klein 7fa8f12132
feat(sync): add Helm chart and WebSocket push for SuperSync (#6971)
* feat(sync): add Helm chart for SuperSync Kubernetes deployment

Includes Deployment, StatefulSet (PostgreSQL), Service, Ingress,
ConfigMap, Secret, HPA, PDB, NetworkPolicy, and test templates.
Supports both bundled PostgreSQL and external database configurations.

* feat(sync): add WebSocket push notifications for near-realtime sync

Server: Fastify WebSocket plugin with connection manager, app-level
heartbeat (30s), debounced notifications, and per-user routing.
Client: WebSocket service with exponential backoff reconnection,
WS-triggered download service, and reduced polling when connected.

* fix(sync): improve WebSocket error handling and reactivity

Make syncInterval$ reactive to WS connection state, fix Set/Map
mutation during heartbeat iteration, add error handling to WS route
handler, separate JSON parsing from message handling, detect auth
failures in WS-triggered downloads, and add logging to all empty
catch blocks.

* fix(sync): address PR review findings for WebSocket and Helm

Fix race condition in WS-triggered download pipeline by moving
isSyncInProgress filter after debounce and adding guard in
_downloadOps. Add logging to remaining empty catch blocks, fix
missing $NODE_IP in Helm NOTES.txt, and correct inaccurate
comments in values.yaml and ws-triggered-download.service.ts.

* fix(sync): add rate limiting to WebSocket upgrade endpoint

Limit WS connection attempts to 10 per minute per IP to prevent
connection flooding, matching the rate-limit pattern used by other
sync endpoints.

* fix(sync): extract shared constants, fix debounce correctness, replace deprecated toPromise

Extract CLIENT_ID_REGEX and MAX_CLIENT_ID_LENGTH into sync.const.ts
to prevent drift between sync.routes.ts and websocket.routes.ts.

Fix debounce in notifyNewOps to accumulate excluded client IDs across
rapid calls from different clients, preventing self-notifications.

Replace deprecated toPromise() with firstValueFrom in connectWebSocket
and _sync methods. Add .catch() to reconnect path and logging to
silent early returns in _downloadOps.

* fix(build): restore correct package-lock.json and fix upstream lint errors

* revert: restore upstream HTML formatting to match CI prettier config

* fix(sync): use DownloadOutcome discriminated union in WsTriggeredDownloadService

* fix(sync): narrow TokenVerificationResult before accessing userId

* fix(sync): await async getProviderById in connectWebSocket

Missing await caused the Promise object to be cast to
SuperSyncProvider, making getWebSocketParams undefined.

* feat(sync): add SEED_USERS env var to create verified users on startup

For self-hosted single-user setups: set SEED_USERS=email1,email2 to
create verified users on boot and log their access tokens. Skips
existing users. Removes need for SMTP/magic link registration.

Also fix Dockerfile to use npm install instead of npm ci for lockfile
compatibility.

* fix(sync): address PR review feedback for Helm chart and server

Security:
- Remove seed-users.ts (logged full JWT tokens to stdout)
- Add fail assertions for missing jwtSecret and postgresql.password
- Add smtp.user/smtp.password values fields
- Add from: selector to NetworkPolicy ingress
- Add egress rule for external database when postgresql.enabled=false

Correctness:
- Fix PostgreSQL StatefulSet indentation for non-persistent mode
- Fix NOTES.txt panic on empty tls list (use len instead of index)
- Restore npm ci in Dockerfile by using node:24-alpine (ships npm 11)
- Add Recreate deployment strategy when using RWO PVC

Operational:
- Add fail guard preventing HPA maxReplicas > 1 (in-memory WS state)
- Fix PDB to use maxUnavailable instead of minAvailable
- Add WebSocket ingress timeout annotation examples
- Add Prisma db push init container for schema migrations
- Default serviceAccount.automount to false
- Add Chart.yaml maintainers, home, sources metadata

* feat(sync): add ALLOWED_EMAILS env var to restrict registration

Supports fully qualified emails (user@example.com) and domain
wildcards (*@example.com). When unset, all emails are allowed.
Applied to all three registration endpoints (passkey options,
passkey verify, magic link).

* fix(sync): exempt health endpoint from rate limiting

Kubernetes liveness/readiness probes hit /health every 5-15s,
exhausting the global rate limit (100 req/15min) and causing
429 responses that trigger container restarts.

* fix(sync): harden WebSocket, Helm chart, and sync services from multi-agent review

- Pin prisma@5.22.0 in init container and use migrate deploy instead of db push
- Add per-user WebSocket connection limit (max 10) to prevent resource exhaustion
- Add replicaCount > 1 fail guard in deployment template (in-memory WS state)
- Set maxPayload: 1024 on WebSocket plugin (only pong messages expected)
- Remove premature IN_SYNC status from download-only WS-triggered sync
- Fix double removeConnection on error+close events (let close handle cleanup)
- DRY debounce logic in notifyNewOps and store latestSeq on pending entry
- Remove dead fromClientId from NewOpsNotification and WsMessage interfaces
- Add takeUntilDestroyed to _wsProviderCleanup subscription
- Simplify email-allowlist.ts to eager init (eliminate mutable state)
- Guard connectWebSocket at call site for non-SuperSync providers
- Add distinctUntilChanged to syncInterval$ to prevent unnecessary resets
- Add container-level securityContext to PostgreSQL StatefulSet
- Fix HPA maxReplicas default to 1 (matches single-replica constraint)

* fix(sync): restore migration in Dockerfile CMD for non-Helm Docker users

Helm users get migration via init container (runs first, CMD becomes no-op).
Docker-compose/plain Docker users get automatic migration back on startup.

* fix(boards): add missing drag delay for touch and extract shared signal

Add cdkDragStartDelay to board-panel drag items, which was missing
entirely. Extract the repeated `isTouchActive() ? DRAG_DELAY_FOR_TOUCH : 0`
expression into a shared `dragDelayForTouch` computed signal and refactor
all 8 components to use it.

* test(sync): add comprehensive WebSocket test coverage

Add unit tests for the new WebSocket real-time sync notification pipeline:

- WebSocketConnectionService (server): connection lifecycle, max-per-user
  limits, notification debouncing, heartbeat, graceful shutdown (13 tests)
- WebSocket routes validation: token/clientId validation, close codes,
  error handling, validation ordering (18 tests)
- SuperSyncWebSocketService (frontend): reconnection with exponential
  backoff, heartbeat, disconnect cleanup, URL conversion (6 tests)
- WsTriggeredDownloadService: auth error handling, pipeline resilience,
  start() idempotency (3 tests)
- SyncWrapperService: connectWebSocket guards for non-SuperSync, null
  params, and already-connected cases (3 tests)
- E2E realtime push: verifies WS-triggered download between two clients

Quality fixes:
- Replace waitForTimeout with syncAndWait in E2E
- Add explanatory comment for microtask flushing in sync-wrapper spec
- Use getter for isSyncInProgress mock in ws-triggered-download spec

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-03-30 21:34:30 +02:00

228 lines
6.4 KiB
YAML

# -- Number of SuperSync replicas.
# Running more than 1 replica requires a shared store (e.g., Redis) for in-memory caches and WebSocket connection state (not yet supported).
replicaCount: 1
image:
# -- SuperSync container image repository
repository: ghcr.io/super-productivity/supersync
# -- Image pull policy
pullPolicy: IfNotPresent
# -- Overrides the image tag. Defaults to the chart appVersion.
tag: ""
# -- Docker registry pull secrets
imagePullSecrets: []
# -- Override the release name prefix
nameOverride: ""
# -- Override the full release name
fullnameOverride: ""
serviceAccount:
# -- Whether to create a ServiceAccount
create: true
# -- Automount the service account token (not needed by this app)
automount: false
# -- Annotations to add to the ServiceAccount
annotations: {}
# -- The name of the ServiceAccount to use. If not set and create is true, a name is generated.
name: ""
# -- SuperSync application configuration
supersync:
# -- Application listening port
port: 1900
# -- Node.js environment
nodeEnv: production
# -- REQUIRED: Public URL of the sync server (e.g. https://sync.example.com)
publicUrl: ""
cors:
# -- Comma-separated list of allowed CORS origins
origins: "https://app.super-productivity.com"
webauthn:
# -- WebAuthn relying party display name
rpName: "Super Productivity Sync"
# -- WebAuthn relying party ID (e.g. sync.example.com)
rpId: ""
# -- WebAuthn origin (e.g. https://sync.example.com)
origin: ""
smtp:
# -- Enable SMTP email sending
enabled: false
# -- SMTP server hostname
host: ""
# -- SMTP server port
port: 587
# -- Use TLS for SMTP
secure: false
# -- Sender email address
from: ""
# -- SMTP username
user: ""
# -- SMTP password
password: ""
# -- Use an existing Kubernetes secret for SMTP credentials.
# The secret must contain SMTP_USER and SMTP_PASS keys.
existingSecret: ""
privacy:
# -- Contact name for privacy policy
contactName: ""
# -- Street address for privacy policy
addressStreet: ""
# -- City for privacy policy
addressCity: ""
# -- Country for privacy policy
addressCountry: ""
# -- Contact email for privacy policy
contactEmail: ""
persistence:
# -- Enable persistent storage for /app/data
enabled: true
# -- Size of the persistent volume
size: 1Gi
# -- Storage class for the PVC. Empty string uses the default storage class.
storageClass: ""
# -- Access modes for the PVC
accessModes:
- ReadWriteOnce
secret:
# -- REQUIRED: JWT signing secret (minimum 32 characters)
jwtSecret: ""
# -- Use a pre-existing Kubernetes secret instead of creating one.
# The secret must contain a JWT_SECRET key.
existingSecret: ""
# -- Built-in PostgreSQL configuration
postgresql:
# -- Deploy a PostgreSQL instance alongside SuperSync.
# Set to false and configure externalDatabase to use an external PostgreSQL.
enabled: true
image:
# -- PostgreSQL image repository
repository: postgres
# -- PostgreSQL image tag
tag: "16-alpine"
# -- PostgreSQL image pull policy
pullPolicy: IfNotPresent
# -- PostgreSQL database name
database: supersync
# -- PostgreSQL username
username: supersync
# -- PostgreSQL password (REQUIRED when postgresql.enabled is true)
password: ""
# -- Use an existing Kubernetes secret for the PostgreSQL password.
# The secret must contain a POSTGRES_PASSWORD key.
existingSecret: ""
persistence:
# -- Enable persistent storage for PostgreSQL data
enabled: true
# -- Size of the PostgreSQL persistent volume
size: 5Gi
# -- Storage class for the PostgreSQL PVC. Empty string uses the default.
storageClass: ""
# -- Access modes for the PostgreSQL PVC
accessModes:
- ReadWriteOnce
# -- Resource requests and limits for PostgreSQL
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 512Mi
# -- External database configuration (used when postgresql.enabled is false)
externalDatabase:
# -- Full PostgreSQL connection URL (e.g. postgresql://user:pass@host:5432/dbname)
url: ""
# -- Use an existing Kubernetes secret for the database URL.
# The secret must contain a DATABASE_URL key.
existingSecret: ""
# -- Ingress configuration
ingress:
# -- Enable ingress
enabled: false
# -- Ingress class name
className: ""
# -- Ingress annotations
# WebSocket support requires extended timeouts. Example for nginx-ingress:
# nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
# nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# For Traefik:
# traefik.ingress.kubernetes.io/router.middlewares: ...
annotations: {}
# -- Ingress host rules
hosts:
- host: sync.example.com
paths:
- path: /
pathType: Prefix
# -- Ingress TLS configuration
tls: []
# - secretName: supersync-tls
# hosts:
# - sync.example.com
# -- Service configuration
service:
# -- Service type
type: ClusterIP
# -- Service port
port: 80
# -- Resource requests and limits for the SuperSync container
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
# -- Horizontal Pod Autoscaler configuration
autoscaling:
# -- Enable HPA
enabled: false
# -- Minimum number of replicas
minReplicas: 1
# -- Maximum number of replicas (must be 1 until shared WS state is implemented)
maxReplicas: 1
# -- Target CPU utilization percentage
targetCPUUtilizationPercentage: 80
# -- Pod Disruption Budget configuration
podDisruptionBudget:
# -- Enable PDB
enabled: false
# -- Maximum number of unavailable pods (use this instead of minAvailable for single-replica)
maxUnavailable: 1
# -- Network Policy configuration
networkPolicy:
# -- Enable NetworkPolicy
enabled: false
# -- Namespace of the ingress controller (for ingress from: selector)
ingressNamespace: "ingress-nginx"
# -- Node selector for pod scheduling
nodeSelector: {}
# -- Tolerations for pod scheduling
tolerations: []
# -- Affinity rules for pod scheduling
affinity: {}
# -- Annotations to add to pods
podAnnotations: {}
# -- Labels to add to pods
podLabels: {}
# -- Pod-level security context
podSecurityContext:
fsGroup: 1001
# -- Container-level security context
securityContext:
runAsUser: 1001
runAsGroup: 1001
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL