Self-hosting
Run sp00ky on your own server with Docker Compose. SurrealDB and one SSP is a complete setup, big projects included.
A self-hosted sp00ky is two containers. SurrealDB holds your data, and the SSP keeps every live query up to date. Your app connects to SurrealDB directly, so the SSP never has to be reachable from the internet.
sequenceDiagram
participant App as Your app
participant DB as SurrealDB
participant SSP
App->>DB: reads and writes over wss://db.example.com
DB->>SSP: every change, through the sync events
SSP->>DB: updated query results
DB-->>App: live update
A single SSP serves every live query for every connected user, and also runs your jobs, schedules, workflows, backend health checks and backups. It carries large projects on its own. Add a scheduler when you want the operator dashboard, and a second SSP only when one machine runs out of memory or CPU.
What you need
- A Linux server with Docker and Docker Compose, ports 80 and 443 open.
- A domain for the database, for example
db.example.com, with its DNS pointing at the server. Caddy uses it to get a TLS certificate automatically. - Your sp00ky project and the
spkyCLI on your own machine.
Set it up
- Write the .env file
On the server, create an empty directory with a
.envfile in it. Compose reads it automatically, and every value below is used in the next steps..env - Add the Caddyfile and docker-compose.yml
Caddy terminates TLS for your domain and forwards it to SurrealDB, WebSockets included. If you already run a reverse proxy, drop the
caddyservice and point your proxy atsurrealdb:8000instead.Caddyfiledocker-compose.ymlSPKY_DB_NSandSPKY_DB_NAMEmust matchsurrealdb.namespaceandsurrealdb.databasein yoursp00ky.yml. Both default tomain. - Start it
The SSP answers with a short JSON status. Once it contains
"status":"ready", it is up. - Apply your schema
From your project directory on your own machine, apply your migrations together with the sync events sp00ky adds to every table:
SURREAL_URLis how your machine reaches SurrealDB.--endpointis how SurrealDB reaches the SSP. The sync events call it on every write, from inside the Docker network, so it is the service namesspand not your domain.--secretmust be the exactSPKY_AUTH_SECRETfrom.env, or the SSP rejects every change.
- Point your app at it
Set the client’s endpoint to your domain. Namespace and database are the same as above.
If your client sets
enableAnonymousLiveQueries: true, addSPKY_SSP_ANON_LIVE_QUERIES: "true"to the SSP as well. The two must agree, or signed-out users get no live updates.
That is a complete production setup. Everything below is optional.
Everyday tasks
Change the schema
Create a migration as usual, then apply it with the same command as the first time:
The SSP notices new and changed tables on its own within 15 seconds. No restart needed.
Upgrade
Update the CLI, set SPOOKY_VERSION in .env to the version it prints, then
pull the new images and re-apply so the sync events match the new version:
To upgrade the scheduler or an SSP without downtime, start the new container next to the old one and let it take over, see Blue/green handover.
Back up
Add the S3_* variables from Backup storage to the SSP and
it can export SurrealDB to any S3-compatible bucket and restore from it,
through the backup routes. With
the scheduler, the dashboard does this for you.
Backends and jobs
If your sp00ky.yml has backends that take jobs from an
outbox table, run each one as another service in the same compose file and tell
the SSP where to send the jobs:
table is the backend’s outbox table (method.table in sp00ky.yml) and
base_url is how the SSP reaches the backend on the Docker network. With no
backends, leave SPKY_JOB_CONFIG out.
Add the scheduler
The scheduler sits between SurrealDB and your SSPs. You still run one SSP. What you get:
- The operator dashboard and its MCP server.
- A replica of your database, so an SSP restart loads its own row checkpoints and repairs only what changed against the replica, instead of reading everything from SurrealDB again.
- An end-to-end heartbeat that can page you through a webhook, and drift checks that repair the replica on their own.
- The option to run more SSPs later.
To switch:
- Set
mode: clusterin yoursp00ky.yml. - Add
SPKY_ADMIN_PASSWORD(another long random value) to.env. - Add the
schedulerservice and point the SSP at it:
- Run
docker compose up -d, then apply again with the scheduler as the endpoint, since the sync events now go to the scheduler instead of the SSP:
The scheduler listens on two ports and they are not equivalent. Port 9667
is the ingest plane, and every route on it is unauthenticated by design because
it is meant to be reachable only from inside your private network. That
includes POST /proxy/query, which runs arbitrary SurrealQL against the
replica for any caller that can open a socket to it. Publishing 9667 hands
your database to the internet.
Port 9668 is the admin plane. It is authenticated, and it is the only scheduler port you ever expose.
Operator dashboard
The dashboard shows the health of the scheduler and every SSP, end-to-end sync latency, backend health, live logs and workflow runs. It can restart SSPs, cancel and retry workflow runs, pause schedules and take backups.
The compose file above binds it to the server itself. Reach it over an SSH tunnel:
Or give it its own domain by adding a second block to the Caddyfile:
Sign in with SPKY_ADMIN_PASSWORD, the break-glass login that works even when
the database is down, or with a normal app account after spky admin add <user>
puts it on the admin roster.
The same port serves the MCP server at /admin/api/mcp,
so an AI assistant can read and operate the deployment with tokens minted on
the dashboard’s Access page.
A few actions only work on Sp00ky Cloud, because they manage containers rather
than processes: pulling newer images, wiping the scheduler volume, restarting
SurrealDB, and the managed backup catalog, schedule and retention. The
dashboard shows them greyed out with the reason Not linked to Sp00ky Cloud.
See Admin dashboard for the full surface.
Run more than one SSP
Only when one SSP runs out of memory or CPU. Each extra SSP needs its own id, address and volume:
The scheduler spreads views across the SSPs by load. Keep
SPKY_CLEAR_VIEWS_ON_START at its default (true) while you run more than one.
Environment variables
Every variable the SSP and the scheduler read, with its default. The setups above only need the handful they show; reach for these when you tune or debug.
SSP (Sidecar)
Read straight from the process environment.
| Variable | Description | Default |
|---|---|---|
SPKY_SSP_LISTEN_ADDR | Address and port for the SSP HTTP server | 0.0.0.0:8667 |
SPKY_SSP_REPLACES | Blue/green: id of the serving SSP this one replaces. It registers as that SSP’s standby and is promoted in its place once caught up (handover) | (unset) |
SPKY_AUTH_SECRET | Bearer token for authenticating SSP API requests. When unset the auth middleware accepts any bearer (dev only). | (empty) |
SPKY_DB_URL | SurrealDB URL (HTTP engine; ws:// values are accepted and normalized) | http://127.0.0.1:8000 |
SPKY_DB_WS | Legacy fallback for SPKY_DB_URL (any scheme accepted) | (unset) |
SPKY_DB_USER | SurrealDB username | root |
SPKY_DB_PASS | SurrealDB password | root |
SPKY_DB_NS | SurrealDB namespace | test |
SPKY_DB_NAME | SurrealDB database | test |
SPKY_SCHEDULER_URL | Scheduler URL (enables distributed mode) | (unset) |
SPKY_SSP_ID | Unique SSP identifier | ssp-<uuid> |
SPKY_SSP_ADVERTISE_ADDR | Externally reachable host:port for this SSP | (unset) |
HEARTBEAT_INTERVAL_MS | Heartbeat interval in ms | 5000 |
TTL_CLEANUP_INTERVAL_SECS | View TTL cleanup interval | 60 |
SPKY_SCHEMA_POLL_SECS | How often (seconds) the SSP probes upstream’s schema and applies added, changed and removed tables in place, with no restart or rebuild. 0 turns the poll off; a view registration naming a table the SSP does not know yet still refreshes. | 15 |
SPKY_INGEST_TRANSPORT | Standalone mode only (no SPKY_SCHEDULER_URL): changefeed makes the SSP tail SurrealDB’s CHANGEFEED itself instead of receiving /ingest posts from the DB events. Takes the same SPKY_CHANGEFEED_* knobs as the scheduler. In cluster mode the scheduler tails. | http |
SPKY_SSP_REF_MODE | _00_list_ref storage layout: dedicated (per-user tables; works around the SurrealDB v3 LIVE permission gap) or single (legacy shared table). Mirrors refMode in sp00ky.yml. | dedicated |
SPKY_SSP_QUERY_UPDATE_THROTTLE_MS | Window (ms) over which the SSP coalesces query edge-update writes to _00_list_ref into one batched transaction, so a burst of view updates lands as a few batched LIVE deliveries instead of one transaction per record. 0 starts each publication immediately; updates arriving during a publication are still merged into the next one. See Architecture. | 20 |
SPKY_JOB_CONFIG | JSON array of the backends the SSP dispatches jobs to: [{"name":"api","base_url":"http://api:3000","table":"job"}], with optional auth_token and timeout per entry. See Backends and jobs. The literal null disables the job runner, so pass [] for none. | (unset) |
SPKY_CRDT_FOLD_IDLE_SECS | CRDT fields: fold a field’s edit log into its canonical document once the log has had no new rows for this long | 5 |
SPKY_CRDT_FOLD_MAX_ROWS | …or once this many log rows are pending, however active the field is | 200 |
SPKY_CRDT_FOLD_MAX_BYTES | …or once this many bytes are pending | 262144 |
SPKY_CRDT_FOLD_MIN_GAP_SECS | Never fold one field more often than this | 10 |
SPKY_CRDT_FOLD_WORKERS | Folds running at once on this SSP | 2 |
SPKY_CRDT_FOLD_TICK_MS | How often the SSP looks for fields due a fold | 5000 |
SPKY_CRDT_PRESENCE_TTL_SECS | @cursor presence rows older than this are removed by the SSP’s periodic sweep | 60 |
SPKY_CRDT_CACHE_SIZE | Hot CRDT documents kept in memory between folds | 1024 |
SPKY_SSP_BOOTSTRAP_PAGE_SIZE | Rows pulled per /proxy/query page | 500 |
SPKY_SSP_REGISTER_MAX_WAIT_SECS | How long to keep retrying scheduler registration before exiting for a restart | 180 |
SPKY_SSP_ANON_LIVE_QUERIES | Allow live queries from unauthenticated clients (1/true to enable) | false |
SPKY_SSP_QUERY_ALLOWLIST | off, warn or enforce. Compare every /view/register shape against the _00_query_allowlist rows the CLI publishes; warn logs and admits a miss, enforce refuses it with 403 not_allowlisted. Mirrors sync.queryAllowlist in sp00ky.yml. See the Query allowlist guide. | off |
SPKY_SSP_VIEW_METRICS_FLUSH_MS | How often view metrics are flushed to the database | 2000 |
SPKY_SSP_MEMORY_LIMIT_MB | Memory ceiling this SSP reports itself against. The heartbeat sends usage as a fraction of it, which is what the scheduler’s LeastLoad strategy ranks on, so an unset or wrong value skews balancing. | 1024 |
OTEL_EXPORTER_OTLP_ENDPOINT | OpenTelemetry collector endpoint for SSP metrics | (unset) |
OTEL_SERVICE_NAME | OTEL service identifier | ssp |
Row checkpoints
Without a snapshot directory every restart of a cluster SSP is a cold rebuild:
the SSP re-pages the whole database while the scheduler holds sync frozen.
Point these at a volume and a restart loads the rows it kept, verifies them
against the scheduler and repairs only what changed. Both are caches of
SurrealDB, never the source of truth, so deleting them is always safe. A
standalone SSP rebuilds from the database on every start; a snapshot.json
an older build left in the directory is removed by the next clean restart.
See State persistence.
| Variable | Description | Default |
|---|---|---|
SPKY_SSP_SNAPSHOT_DIR | Directory for what a cluster SSP keeps across a restart: row checkpoints in rows/, a base image and its deltas per table, verified against the scheduler on boot. Enabled when set and writable; the SSP probes it and falls back to memory-only rather than failing startup. Put it on a volume that outlives the container. | (unset) |
SPKY_SSP_ROW_CHECKPOINT_SECS | Cluster SSP: how often tables whose content changed are written to rows/ while running, as a delta of the rows changed since the last write (whole again after 64 deltas or when they outweigh half the base). 0 leaves only the writes on graceful shutdown and after each bootstrap. | 300 |
SPKY_SSP_CHECKPOINT_VERIFY | Cluster SSP: background verifies the bodies of a loaded checkpoint while the SSP registers and repairs (a table that fails is dropped and paged again); load verifies before a table is served, reading every file whole at boot. | background |
SPKY_SSP_REPAIR_CONCURRENCY | Cluster SSP: how many tables, and within a table how many id-range listings, fetches and re-reads, a warm bootstrap repairs at a time. Each proxy query is one read on a scheduler worker, so the scheduler’s worker count is the useful ceiling. | 4 |
SPKY_SSP_ARENA_DIR | Directory for the row arena, a sparse-file cache of row bytes. Probed for writability like the snapshot dir; falls back to the heap. | (unset) |
SPKY_SSP_ARENA_SEGMENT_MB | Arena segment size in MB | 64 |
Scheduler
The scheduler reads most of its config from sp00ky.yml, and the SPKY_* vars
in the first table below override the corresponding YAML field. The fields
ingest_host, ingest_port, load_balance, replica_db_path, wal_path,
ssp_poll_interval_ms and job_tables are YAML only; max_buffer_per_ssp also
answers to SPKY_MAX_BUFFER_PER_SSP.
The tables after it are env only and have no YAML equivalent: the admin plane, backup storage, the dead-man heartbeat, and the tuning knobs.
Core
| Variable | Description | Default |
|---|---|---|
SPKY_SCHEDULER_ID | Unique scheduler identifier | scheduler-<uuid> |
SPKY_HANDOVER_FROM | Blue/green: base URL of the running scheduler to take over from, on the same data volume. Needs the same SPKY_AUTH_SECRET (handover) | (unset) |
SPKY_HANDOVER_ADVERTISE | Host the predecessor relays to after the handover | the container hostname |
SPKY_DB_URL | SurrealDB URL (HTTP engine; ws:// values are accepted and normalized) | http://localhost:8000 |
SPKY_DB_WS | Legacy fallback for SPKY_DB_URL (any scheme accepted) | (unset) |
SPKY_DB_NS | SurrealDB namespace | sp00ky |
SPKY_DB_NAME | SurrealDB database | sp00ky |
SPKY_DB_USER | SurrealDB username | root |
SPKY_DB_PASS | SurrealDB password | root |
SPKY_SNAPSHOT_UPDATE_INTERVAL_SECS | Snapshot update interval (seconds) | 300 |
SPKY_WAL_SEGMENT_MB | Size at which the scheduler’s event WAL (./data/wal/, append-only segments) rotates to a new file. Drains unlink covered segments instead of rewriting the log. | 16 |
SPKY_DRIFT_CHECK | Compare the replica’s row counts against upstream at startup and after each snapshot drain. Before it accepts SSPs, the scheduler only checks the tables its replica holds no row of (the mismatch an SSP must never bootstrap from); the full count runs right after | true |
SPKY_DRIFT_COUNT_CONCURRENCY | Upstream count() queries one drift check runs at once. Each scans its whole table; 1 counts one table at a time, for an upstream with no CPU to spare | 4 |
SPKY_DRIFT_AUTO_RECLONE | Act when the check confirms a mismatch: repair the table in place by sending the rows that differ through the ingest pipeline, and re-clone the replica (re-bootstrapping every SSP) only when a repair cannot fix it (false = report only) | true |
SPKY_DRIFT_CONFIRM_TICKS | Consecutive checks a non-zero count mismatch must persist before acting (an empty table upstream has rows for is acted on immediately) | 2 |
SPKY_DRIFT_RECLONE_COOLDOWN_SECS | Minimum seconds between automatic re-clones | 3600 |
SPKY_DRIFT_REPAIR_MAX_ROWS | Most rows one in-place repair of a table the replica already held may send; a bigger difference falls back to a re-clone. A table new to the replica (added upstream, or with @nosync removed) is backfilled with no cap, in the background, and never re-cloned for | 2000 |
SPKY_DRIFT_REPAIR_TIMEOUT_SECS | Deadline for one table repair, and the longest any repair (a backfill included) may go without finishing a page; past it the repair is abandoned, the upstream connection replaced, and the repair retried on the next check | 300 |
SPKY_DRIFT_CHECK_TIMEOUT_SECS | Deadline for one drift check; past it the check is abandoned and the upstream connection replaced, so a wedged query cannot stop the replica draining | 120 |
SPKY_BOOTSTRAP_PAGE_SIZE | Rows per page when the replica clone or a drift repair reads a table from upstream; the table is cut into id windows of three quarters of a page | about 32 MiB of rows by a one-row probe, at most 2000 |
SPKY_AUTH_SECRET | Optional shared secret; when set, callers to /proxy/* must send Authorization: Bearer <secret>. Also signs admin dashboard sessions, so setting it is what makes them survive a restart. | (empty) |
SPKY_BACKENDS | JSON-encoded backend health-check list (SPKY_SCHEDULER_BACKENDS accepted as legacy fallback) | (unset) |
SPKY_JOB_CONFIG | The outbox tables whose jobs the scheduler recovers and re-dispatches. Takes the SSP’s array as is (only each table is read) or an object keyed by table. Without it, recurring and delayed jobs are never re-dispatched after their first run. | (unset) |
Admin plane
The dashboard and its MCP server, on their own port. See Admin dashboard.
| Variable | Description | Default |
|---|---|---|
SPKY_ADMIN_ENABLED | Serve the admin plane at all. Set to 0, false or off to disable it; any other value (or unset) enables it. | true |
SPKY_ADMIN_PORT | Port for the admin plane | 9668 |
SPKY_ADMIN_HOST | Bind address for the admin plane | 0.0.0.0 |
SPKY_ADMIN_DIR | Directory holding the built dashboard. The official image ships one here; a scheduler built from a checkout serves a placeholder page instead. | /usr/share/spooky/dashboard |
SPKY_ADMIN_PASSWORD | Break-glass password, which signs in without the _00_admin roster. An empty value is treated as unset, so a deployment that templates the var but leaves it blank does not end up with a guessable password. | (unset) |
SPKY_ADMIN_ACCESS | Pins the SurrealDB record-access method used for admin sign-in. When unset the accesses are discovered from INFO FOR DB. | (auto) |
SPKY_ADMIN_SESSION_TTL_SECS | How long a signed-in session lasts | 28800 (8h) |
SPKY_ADMIN_PRESENCE_INTERVAL_SECS | How often the presence sampler reads _00_query, which feeds the Overview charts and the Views tab. One sampler serves every open dashboard, so this is the total cost however many people are watching. | 15 |
SPKY_ADMIN_SLOW_VIEW_MS | Materialization p99 at which a registered view is counted as slow. Only the default; GET /admin/api/views?slow_ms= overrides it per request. | 250 |
SPKY_ADMIN_LARGE_VIEW_ROWS | Row count from which a registered view is flagged as large. The Views tab and the Overview warn on these because an unwindowed live view republishes every row as a _00_list_ref edge on each cold registration, in one transaction, and a few thousand of those at once has stalled a tenant’s SurrealDB. Window such queries (LIMIT/START) or resolve rows by id. GET /admin/api/views?large=true lists them. | 1000 |
SPKY_ADMIN_HOT_VIEW_UPDATES_PER_SEC | Updates per second, measured between two presence samples, from which a registered view is flagged as hot. The Views tab and the Overview warn on these and a hot_views incident stays open while any view is hot, because a view changing this often (typically a bulk import into a table someone has open) floods its SSP’s publication queue. GET /admin/api/views?hot=true lists them. | 5 |
SPKY_ADMIN_PRESENCE_MAX_ROWS | Ceiling on the rows one presence or views query pulls back, so a tenant with a runaway number of registrations cannot make the sampler the expensive thing on the box. Above it the figures are reported as a floor. | 20000 |
SPKY_ADMIN_JOB_INTERVAL_SECS | How often the job sampler reads your outbox tables, feeding the Jobs page, its totals and the Overview tile. Every job aggregate is a scan of one of your own tables, so one sampler takes them for every open dashboard. Set it to 0 to switch the sampler off entirely: the Jobs list and its filters still work, and the totals then report that they are not being measured rather than showing zeros. | 15 |
SPKY_ADMIN_DATA | Serve /admin/api/data: the project’s tables, users and bucket files, as root. Set to 0, false, off or no to refuse it. | on |
SPKY_BUCKET_DIR | A directory holding your file: buckets, mounted read-only into the scheduler at the same path SurrealDB uses. SurrealDB does not list directories, so without it the Storage page opens a subfolder only by its path. Writes still go through SurrealDB. | (unset) |
SPKY_ADMIN_JOB_LIVE_INTERVAL_SECS | The cadence while at least one dashboard holds the Jobs stream open, so a backlog can be watched draining. Clamped to no slower than the idle interval. | 2 |
Machine pools
The listener machine pool agents connect to, and the docker provider a self-hosted scheduler can create machines with.
| Variable | Description | Default |
|---|---|---|
SPKY_POOL_ENABLED | Serve pools at all. 0 or false turns the listener and the pool sweep off. | true |
SPKY_POOL_PORT | Port of the pool listener | 9669 |
SPKY_POOL_HOST | Bind address of the pool listener | 0.0.0.0 |
SPKY_POOL_PUBLIC_URL | The listener’s URL as a machine reaches it. Handed to every machine the scheduler creates. | http://scheduler:<port> |
SPKY_POOL_SECRET | Key that signs each machine’s token. Must survive restarts, or running machines are locked out and their jobs fail over. Falls back to SPKY_AUTH_SECRET. | (SPKY_AUTH_SECRET) |
SPKY_POOL_DOCKER | Enable the docker provider: machines are containers on this scheduler’s Docker host. Needs the Docker socket mounted. | (off) |
SPKY_POOL_DOCKER_NETWORK | Docker network machine containers join, so they can reach SPKY_POOL_PUBLIC_URL | (default bridge) |
SPKY_POOL_AGENT_IMAGE | Image carrying the spky-agent binary each machine runs. Defaults to the agent released with this scheduler, which is the one that speaks its protocol. | mono424/spooky-agent:<release> |
Backup storage
These carry no SPKY_ prefix. The backup plane is enabled as soon as any of
S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY or S3_BUCKET is present, so a
partial configuration turns backups on with defaults filling the rest. Any
S3-compatible store works. Also read by a standalone SSP, which serves the same
backup routes.
| Variable | Description | Default |
|---|---|---|
S3_ENDPOINT | S3-compatible endpoint | http://10.100.1.5:9000 |
S3_ACCESS_KEY | Access key | minioadmin |
S3_SECRET_KEY | Secret key | minioadmin |
S3_BUCKET | Bucket backups are written to | backups |
S3_REGION | Region | us-east-1 |
SPKY_PROJECT_SLUG | Prefix backups are stored under, as <slug>/<backup-id>.surql.gz. Also what the dashboard lists when it is not linked to Sp00ky Cloud. | default |
Heartbeat and alerting
An end-to-end probe: the scheduler writes _00_heartbeat:probe upstream and
times the full round trip through /ingest, the WAL, the broadcast and every
SSP’s circuit. It is what the dashboard’s latency reading and the /health
staleness flag are built on.
| Variable | Description | Default |
|---|---|---|
SPKY_HEARTBEAT_INTERVAL_SECS | Seconds between probe cycles. 0 disables the probe. | 30 |
SPKY_HEARTBEAT_TIMEOUT_SECS | Deadline for one cycle. Clamped below the interval so a cycle always finishes before the next tick. | 25 |
SPKY_HEARTBEAT_FAIL_THRESHOLD | Consecutive failed cycles before the alert fires | 3 |
SPKY_HEARTBEAT_PING_URL | Dead-man URL pinged after each successful cycle, for an external uptime monitor | (unset) |
SPKY_ALERT_WEBHOOK_URL | Webhook posted to when the probe crosses the failure threshold and again when it recovers | (unset) |
SPKY_HEALTH_MAX_HEARTBEAT_AGE_SECS | How stale the last successful probe may be before /health degrades. Floored at 3 x interval + timeout, so a smaller value has no effect. | (computed) |
SurrealDB WebSocket watchdog
Each cycle the scheduler opens a new WebSocket to SurrealDB and waits for the
upgrade. A cycle counts as wedged when /health answers but the upgrade gets
no response at all, the SurrealDB 3.x state where open sockets keep working
and no new client can connect. A database that is down is left to the control
plane. Needs a plain http:// or ws:// SPKY_DB_URL; restarts need the
Sp00ky Cloud link, without it the watchdog only reports.
| Variable | Description | Default |
|---|---|---|
SPKY_SURREAL_WS_WATCHDOG_INTERVAL_SECS | Seconds between probe cycles. 0 disables the watchdog. | 30 |
SPKY_SURREAL_WS_WATCHDOG_TIMEOUT_SECS | How long one upgrade may take before the cycle counts as wedged. Clamped below the interval. | 10 |
SPKY_SURREAL_WS_WATCHDOG_INCIDENT_AFTER | Wedged cycles in a row before the ws_accept_wedge admin incident opens, so one slow upgrade on an overloaded database is not recorded as a wedge. Clamped to 1 through the fail threshold, so a restart is always recorded on an open incident. | 2 |
SPKY_SURREAL_WS_WATCHDOG_FAIL_THRESHOLD | Wedged cycles in a row before the watchdog asks Sp00ky Cloud to restart SurrealDB | 3 |
SPKY_SURREAL_WS_WATCHDOG_COOLDOWN_SECS | Minimum seconds between two restart requests | 900 |
SPKY_SURREAL_WS_WATCHDOG_RESTART | 0, false, off or no = report only, never restart | true |
Tuning
| Variable | Description | Default |
|---|---|---|
SPKY_WORKER_THREADS | Tokio worker threads | 4 |
SPKY_HTTP_TIMEOUT_SECS | Request deadline on the ingest plane, so a wedged handler produces a 408 rather than a hung connection. The admin plane is exempt because its log and event streams are meant to stay open. | 120 |
SPKY_HEALTH_MAX_LAG | Ingest lag above which /health reports degraded. Unset means lag never degrades health. | (unset) |
SPKY_CLONE_TIMEOUT_SECS | Ceiling on the initial replica clone. Overrunning it fails startup and exits, rather than leaving the scheduler wedged in cloning and answering 503 to every SSP for the life of the container. | 900 |
SPKY_BOOTSTRAP_TIMEOUT_SECS | How long an SSP may take to bootstrap before the scheduler reaps it. Also half of what the dashboard waits on before calling a restart failed. | 300 |
SPKY_MAX_BUFFER_PER_SSP | Events the scheduler queues for one SSP while it bootstraps or lags. Past it the queue is dropped and the SSP is told to re-bootstrap, from a fresh registration with an empty queue. Size it for the write rate times the longest bootstrap: 50,000 is five minutes at 166 events/s. | 50000 |
SPKY_CLEAR_VIEWS_ON_START | Wipe every registered view (_00_query) when the scheduler starts. false keeps them: the replica clones them, the SSPs re-register them at bootstrap, and clients keep their views across a scheduler restart with no re-registration; the SSP TTL sweep retires rows nobody heartbeats. Leave true on tenants with more than one SSP for now (every SSP would compute every view). | true |
SPKY_INGEST_TRANSPORT | http (the generated DB events post to /ingest) or changefeed (the scheduler tails SurrealDB’s CHANGEFEED; must match the schema deployed with sync.transport). See Sync transport. | http |
SPKY_CHANGEFEED_RETENTION | The schema’s CHANGEFEED retention, rendered form (1d). A cursor older than this minus the margin is a gap: the replica is re-cloned. | 1d |
SPKY_CHANGEFEED_DOORBELL | LIVE SELECT id FROM _00_version over WebSocket as the wake-up for the tail (plus a best-effort one on _00_push_message, so direct pushes do not wait for the fallback poll); false = poll only. | true |
SPKY_CHANGEFEED_DEBOUNCE_MS | Wait after a doorbell wake before polling, so a bulk transaction costs one poll. | 5 |
SPKY_CHANGEFEED_FALLBACK_MS | Safety-net poll interval while the doorbell is connected. | 2000 |
SPKY_CHANGEFEED_FALLBACK_DOWN_MS | Poll interval while the doorbell is reconnecting. | 250 |
SPKY_CHANGEFEED_GAP_MARGIN_SECS | Clock-skew allowance subtracted from the retention in the gap check. | 300 |
SPKY_CHANGEFEED_POLL_LIMIT | Feed keys per SHOW CHANGES (SurrealDB counts one key per transaction and table). A poll that ends mid-transaction is re-read from that transaction. | 500 |
SPKY_CHANGEFEED_POLL_TIMEOUT_SECS | Deadline for one SHOW CHANGES; past it the request is abandoned and the tail’s session replaced. Also the ceiling of the outage a wedged SHOW CHANGES can cause on SurrealDB 3.1.5, so keep it short. | 5 |
SPKY_BOOTSTRAP_RECLONE_AFTER | Consecutive bootstrap integrity failures before the replica is re-cloned from upstream | 3 |
SPKY_BOOTSTRAP_ADMIT_AFTER | Consecutive bootstrap integrity failures before the SSP is admitted to broadcast anyway, restoring sync. Floored at the re-clone threshold, so admit never precedes a re-clone attempt. | 5 |
SPKY_CATCHUP_RECLONE_AFTER | Same escalation for the catch-up gate | 3 |
SPKY_CATCHUP_ADMIT_AFTER | Same escalation for the catch-up gate | 5 |
Web Push
The push engine runs in the scheduler in cluster mode and in the SSP in
singlenode mode (an SSP behind a scheduler never runs it), and reads the same
variables in both. It needs no configuration: its VAPID key is derived from
SPKY_AUTH_SECRET, and the rules come from push: in sp00ky.yml. See
Push notifications.
Native push needs no variable either: the APNs and FCM credentials come from
push.apns / push.fcm, which spky migrate stores in the database
(_00_push_credential). The host needs outbound HTTPS (HTTP/2 for Apple) to
api.push.apple.com, api.sandbox.push.apple.com, fcm.googleapis.com and
oauth2.googleapis.com. See Native push.
| Variable | Description | Default |
|---|---|---|
SPKY_PUSH | off (or 0, false, no, disabled) switches the engine off, web and native. Subscriptions and queued messages are kept. | (on) |
SPKY_VAPID_PRIVATE_KEY | A VAPID private key (base64url raw P-256 scalar, the form web-push libraries print) to use instead of the one derived from SPKY_AUTH_SECRET. Set it on every host that runs the engine. Changing the key (or the secret it is derived from) invalidates every browser subscription: clients re-subscribe on their next boot through db.webPush.sync(). With neither this nor SPKY_AUTH_SECRET, push is off. | (derived) |
SPKY_PUSH_ALLOW_PRIVATE_ENDPOINTS | Accept http:// endpoints and private or local addresses, for a mock push service in development. Never in production: endpoints are chosen by your users and the engine posts to them from inside your network. | false |
SPKY_PUSH_SEND_CONCURRENCY | Concurrent HTTP sends per matched row or tick | 8 |
SPKY_PUSH_DEDUPE_CAPACITY | Rule matches remembered to drop duplicates (a row re-delivered by the changefeed look-back or a restart) | 100000 |
SPKY_PUSH_CONFIG_RELOAD_SECS | How often the stored push: block is re-read, so a deploy’s rule change is live within this | 30 |
SPKY_PUSH_DEFAULT_TTL_SECS | Push-service TTL when neither the rule nor push.defaults.ttl sets one | 86400 |
A managed deployment also receives SPKY_CLOUD_API_URL and
SPKY_CLOUD_PROJECT on the scheduler container. They are what let the
dashboard ask the control plane to upgrade images, wipe the volume or manage
the backup catalog, authenticated with the project’s own SPKY_AUTH_SECRET.
Self-hosted schedulers leave them unset and report cloud_linked: false.