Skip to content

powersync-community/powersync-helm-chart

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PowerSync Helm Chart

This Helm chart deploys PowerSync services on a Kubernetes cluster.

Prerequisites

  • Kubernetes 1.21+
  • Helm 3.0+ (Helm 4 also supported and verified by the local test pipeline — see Local testing)
  • A bucket-storage database (MongoDB or Postgres) and a source database
  • An NGINX-compatible Ingress controller (or any L7 controller with HTTP/2 + WebSockets)
  • For autoscaling on powersync_concurrent_connections: Prometheus + prometheus-adapter, or KEDA

Installing the Chart

# Copy the default values and customize them
cp values.yaml my-values.yaml
# Edit my-values.yaml with your specific configuration

helm install powersync ./powersync-helm-chart -f my-values.yaml

What gets deployed

Workload Kind Notes
*-api Deployment Sync API. Behind a Service + Ingress. HPA + PDB.
*-replication Deployment Warm-standby (2 replicas) — only one actively replicates; the other waits for the replication lock and takes over on failure. PDB + anti-affinity included.
*-compact CronJob Daily bucket-storage compaction.
*-migrate Job Pre-install/pre-upgrade hook running migrate up.
*-config Secret Renders powersyncConfig to JSON.
*-sync-streams ConfigMap Sync Stream definitions (edition 3 sync config).
* Ingress Ingress TLS + NGINX streaming annotations.
*-api HPA HPA Scales on powersync_concurrent_connections + CPU.
*-api PDB PodDisruptionBudget minAvailable: 1.
*-api NetworkPolicy NetworkPolicy Optional, off by default.

PowerSync specifications

Numbers and constraints from the PowerSync deployment-architecture docs. The chart defaults follow these.

Per-pod limits

Pod Limit What happens at the limit
API ~100 target / 200 hard cap concurrent client connections per pod Pod returns error code PSYNC_S2304 (max concurrent connections reached). Scale out before you hit 200.
Replication 1 active replicator at any time (warm-standby pattern, default 2 pods) The lock holder actively replicates; the standby pod blocks on PSYNC_S1003 and takes over instantly if the leader exits. Mirrors the PowerSync Cloud paid-plan default. Older service versions log noisily on the standby — set replication.replicas: 1 + strategy: Recreate if needed.

Sizing baseline

Component Replicas Memory (req / limit) CPU Notes
API 2+ (HPA) 1Gi / 2Gi 1 vCPU Stateless. Scale horizontally on connection count.
Replication 2 (1 active + 1 warm standby) 1Gi / 2Gi 1 vCPU Lock arbitration ensures only one replicates at a time. Standby is idle (low CPU) but takes over instantly on leader failure.
Compact (CronJob) 1× daily 512Mi / 1Gi 100m / 1 Off-peak.
Bucket storage (Postgres) 3 (1 primary + 2 replicas) 2Gi+ 1+ vCPU Out of scope for this chart — deploy via CloudNativePG.

Scaling rules:

  • API → add 1 pod per ~100 concurrent client connections. HPA does this automatically using powersync_concurrent_connections.
  • Replication → vertical only. Scales with source-database write throughput, not client count.
  • For larger rows / heavier load, double API + replication to 2Gi / 2 vCPU.

Heap sizing

NODE_OPTIONS=--max-old-space-size-percentage=80 — the V8 old-generation heap is sized as 80% of the container memory limit, so it auto-tracks resources.limits.memory. No manual recalculation when you change limits. (Older PowerSync deployment guides hard-code --max-old-space-size=800 for a 1Gi container; the percentage flag is the same idea, just dynamic.)

When you need more than one PowerSync instance

A single PowerSync instance (one replicator + horizontally-scaled API pods) can handle roughly 50,000–100,000 concurrent client connections, depending on the size of the rows being synced from the source database. Beyond that, configure a second instance — a separate deployment with its own bucket-storage database, sharing the same source DB.

Critical client-side constraint: each instance maintains its own copy of the bucket data, so a client must always connect to the same instance every time. Switching instances forces a full resync from scratch. Multiple instances cannot be load-balanced behind the same subdomain. Recommended routing: have the client fetch its endpoint from your backend (or compute it deterministically, e.g. hash(user_id) % n) and pin it.

This chart deploys one instance; for multi-instance, install the chart multiple times under different release names, ingress hosts, and bucket-storage configs.

Health probe endpoints

Defaults use file-system probes (MICRO_PROBE_TYPE=fs, reading /app/.probes/{startup,poll,ready}). HTTP equivalents are also available on port 8080:

Probe HTTP path File path
Startup GET /probes/startup /app/.probes/startup
Liveness GET /probes/liveness /app/.probes/poll
Readiness GET /probes/readiness /app/.probes/ready

To switch to HTTP probes, set env.MICRO_PROBE_TYPE: "http" and edit the deployment templates.

Prometheus metrics

Exposed on container port 9464. Enable in powersyncConfig.telemetry. Key metrics:

Metric Type Use
powersync_concurrent_connections Gauge HPA scaling signal; alert when nearing 200/pod
powersync_replication_lag_seconds Gauge Alert when lag spikes
powersync_operations_synced_total Counter Sync throughput
powersync_data_synced_bytes_total Counter Egress volume (uncompressed)
powersync_data_sent_bytes_total Counter Egress volume (compressed)
powersync_data_replicated_bytes_total Counter Source-DB → bucket-storage volume
powersync_rows_replicated_total Counter Replication throughput
powersync_transactions_replicated_total Counter Replication throughput
powersync_replication_storage_size_bytes Gauge Bucket-storage growth
powersync_operation_storage_size_bytes Gauge Bucket-storage growth

Network requirements

From To Protocol Why
Client Ingress → API HTTPS (long-lived) Streaming API — needs proxy-buffering: off
API pods Bucket-storage DB TCP Read materialised buckets
API pods JWKS endpoint HTTPS (egress) Verify client JWTs
Replication pod Source DB TCP (logical replication / oplog) CDC stream
Replication pod Bucket-storage DB TCP Write materialised buckets
Compact CronJob Bucket-storage DB TCP Daily compaction

Autoscaling on concurrent connections

The chart ships with an HPA disabled by default. Enable with:

api:
  autoscaling:
    enabled: true
    minReplicas: 2
    maxReplicas: 10
    targetConnectionsPerPod: 100
    targetCPUUtilizationPercentage: 70

The HPA reads the powersync_concurrent_connections Prometheus gauge as a Pods-type metric. To make this metric visible to Kubernetes, deploy prometheus-adapter with a rule that maps the metric to the custom.metrics.k8s.io API. Sample rule:

rules:
  custom:
    - seriesQuery: 'powersync_concurrent_connections{namespace!="",pod!=""}'
      resources:
        overrides:
          namespace: { resource: namespace }
          pod: { resource: pod }
      name:
        matches: "^(.*)$"
        as: "$1"
      metricsQuery: 'avg_over_time(<<.Series>>{<<.LabelMatchers>>}[2m])'

Verify it's working:

kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/<ns>/pods/*/powersync_concurrent_connections"

Alternatively, use KEDA's prometheus scaler — set autoscaling.enabled: false and define a ScaledObject outside this chart.

Ingress

The chart sets these NGINX annotations by default — without them, HTTP streaming sync breaks:

nginx.ingress.kubernetes.io/proxy-buffering: "off"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"

Use a dedicated subdomain for the API (e.g. powersync.example.com). PowerSync cannot share a subdomain with another service.

Secrets

powersyncConfig is rendered into a Kubernetes Secret. Storing real credentials in values.yaml is fine for a demo, but for production prefer:

The chart's example client_auth.jwks uses a shared-secret HS256 key for demo purposes only. For production, use asymmetric keys (RS256, EdDSA, ECDSA) via jwks_uri.

Migrations

powersyncConfig.migrations.disable_auto_migration: true is set by default. The *-migrate Job runs migrate up as a Helm pre-install and pre-upgrade hook so migrations execute before pods start. To disable, set migration.enabled: false.

Common values

Parameter Description Default
namespace Kubernetes namespace powersync-poc
image.repository Image repository journeyapps/powersync-service
image.tag Image tag 1.20.5
api.replicas API replicas (when HPA disabled) 2
api.autoscaling.enabled Enable HPA false
replication.replicas Replication pods (warm-standby pattern) 2
replication.podAntiAffinity.enabled Spread replication pods across nodes true
api.autoscaling.targetConnectionsPerPod HPA target 100
api.pdb.enabled Enable PodDisruptionBudget true
compact.enabled Enable daily compact CronJob true
compact.schedule Cron schedule 0 3 * * *
migration.enabled Run migrate-up as a Helm hook true
networkPolicy.enabled Restrict API ingress false
ingress.host Ingress hostname YOUR_FQDN_HERE.example.com

Local testing

The repo ships with a self-contained pipeline that lints and renders the chart, deploys it to a local kind cluster against in-cluster MongoDB fixtures, and runs assertions against the result. Use it before any chart change.

Requirements

Tool Purpose Install
helm (v4+) Chart lint/render/install brew install helm
kind Local Kubernetes cluster brew install kind
kubectl Cluster client brew install kubernetes-cli
kubeconform K8s schema validation brew install kubeconform
yq YAML parsing brew install yq
jq JSON parsing usually preinstalled
Docker Container runtime for kind Docker Desktop / OrbStack

One-liner:

brew install helm kind kubernetes-cli kubeconform yq jq

Quick start

./test/setup-fixtures.sh   # idempotent — kind + ingress-nginx + 2× MongoDB + TLS secret (~3 min)
./test/run.sh --audit      # full pipeline with evidence per assertion
./test/teardown.sh         # delete the kind cluster when finished

What runs

Stage What it does
Static helm lint, helm template -f test/values-test.yaml, kubeconform -strict on the rendered manifests
Deploy helm upgrade --install against the kind cluster, waits for rollout, captures pod state on failure
Smoke Runs every assertion declared in test/assertions.yaml (deployment readiness, file probes, listening ports, migrate Job success, replication leader election, initial replication + streaming, kubeconform, built-in helm test)

Run modes

./test/run.sh                  # quiet PASS/FAIL summary
./test/run.sh --audit          # verbose evidence per assertion
./test/run.sh --only=static    # static stage only (no cluster needed)
./test/run.sh --only=smoke     # re-check assertions against the current install
./test/run.sh --self-test      # failure-injection battery — proves the pipeline catches what it claims

--self-test runs each scenario in test/injections/*.yaml in an isolated git worktree and confirms the right stage/assertion catches each break (template typo, bad memory limit, mismatched ConfigMap name, bad source-DB URI, wrong migrate args, renamed metrics port, bad image tag).

Inspect after a run

Every run dumps evidence to test/.last-run/ (gitignored):

test/.last-run/
├── stages/                          # structured JSON per stage
│   ├── 1-static.json
│   ├── 2-deploy.json
│   └── 3-smoke.json
├── rendered/all.yaml                # helm template output
├── events.log                       # cluster events
├── workload.summary                 # pods/deploys/jobs + leader/standby IDs
└── pods/
    ├── api/                         # one log file per API replica
    ├── migrate.log                  # pre-install Job
    ├── replication-leader.log       # active replicator (initial sync, streaming, resume tokens)
    └── replication-standby.log      # idle warm standby (PSYNC_S1003 lock contention)

Useful greps:

grep -E 'locked for replication|Initial replication|Resume streaming|Idle change stream' test/.last-run/pods/replication-leader.log
grep PSYNC_S1003 test/.last-run/pods/replication-standby.log
jq '.checks[] | select(.ok==false)' test/.last-run/stages/3-smoke.json

Adding a new assertion

test/assertions.yaml is the single source of truth for what the smoke stage tests. Append an entry; the smoke runner picks it up on the next run. Supported types: kubectl (jsonpath), kubectl-exec (cmd in pod), log-grep (pattern + match count), kubeconform, helm-test.

Troubleshooting

# Pod logs
kubectl logs -n powersync-poc -l app=<release>-api
kubectl logs -n powersync-poc -l app=<release>-replication

# Pod status
kubectl get pods -n powersync-poc

# HPA status (when enabled)
kubectl get hpa -n powersync-poc
kubectl describe hpa -n powersync-poc

# Migration job output
kubectl logs -n powersync-poc job/<release>-migrate

References

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors