sp00ky.yml
Every key in the project config the CLI reads: apps, schedules, schema paths, versions and per-environment overrides.
sp00ky.yml at your project root is the single source of truth for the spky CLI. It tells spky dev, spky deploy and spky doctor how to run your stack, which SurrealDB version to spin up, where your schema and buckets live, which client types to generate, and how your apps boot.
For the client-side object you pass to new SyncedDb(...), see Client config.
Validate it at any time with spky lint, or run spky doctor for a fuller
project health check.
Add the schema comment as the first line of sp00ky.yml to get autocomplete and validation in editors that support yaml-language-server:
Minimal example
The smallest viable file to get spky dev running:
For a fully annotated reference covering every option, see example/sp00ky.yml in the repo.
Top-level settings
| Field | Type | Default | Purpose |
|---|---|---|---|
mode | singlenode | cluster | surrealism | singlenode | Deployment topology. singlenode runs one SSP, cluster runs multiple, surrealism is for embedded setups. |
slug | string | (none) | Project slug used by spky deploy and the other cloud commands. |
surrealdb | object | (none) | DB connection. Sub-fields: namespace, database, username, password, hosting (cloud or external), endpoint (required when hosting: external). |
schema | path | (none) | Directory containing your .surql schema sources. See Schema. |
buckets | path[] | [] | Bucket definition files referenced from your schema. See Buckets. |
clientTypes | array | [] | Codegen targets. Each entry has format (typescript or dart), an output path, and (dart only) an optional workdir. See Client types. |
version | string | object | latest stable | Pin SSP and scheduler image tags. Accepts a single tag, a {dev, cloud} split, or a path: override pointing at a local binary. |
logLevel | string | object | info | Log verbosity. Accepts trace, debug, info, warn, error, off, target=level directives, or a {dev, cloud} split. |
refMode | dedicated | single | dedicated | Storage layout for the SSP’s _00_list_ref table. dedicated gives each user their own table and works around a SurrealDB v3 LIVE-permission gap. |
apps | map | {} | Your frontend and backend services. See Apps below. |
schedules | map | path | {} | Server-side cron/interval jobs. See Schedules. |
workflows | map | path | {} | Server-side workflow DAGs. See Workflows. |
deployment | object | (none) | Cloud deployment knobs: sspCount, backup. See Cloud Deployment. |
retention | object | (see below) | How long finished jobs and run history are kept. See Retention. |
Apps
Each entry under apps is a service spky knows how to run locally and deploy to the cloud. Set type: frontend, type: backend, or type: docker, then describe how it boots (dev) and how it deploys (deploy). Backends additionally need an OpenAPI spec, a baseUrl, an optional auth block, and a method (typically outbox) so the SSP can deliver jobs.
method describes the outbox the SSP delivers jobs through: type: outbox, the table the rows live in, the schema file defining it, and an optional concurrency bounding how many of that table’s jobs run at once (default 1; extra rows wait as pending and are admitted oldest-first). See Jobs → Concurrency.
dev accepts either a raw command string or a typed form: {type: npm, script, workdir?}, {type: docker, file, workdir?, port?} (builds a Dockerfile for this app’s dev server), or {type: uv, script, workdir?}. deploy accepts dockerfile, context, port, healthcheck, resources (vcpus, memory, disk), a timeout, a cmd (override the image’s ENTRYPOINT/CMD), build_args (build-time-only args, same shape as env), and static ({build, dir}, for Cloudflare static-asset frontends).
scope: where an app runs
Every app accepts an optional scope controlling where it runs:
scope | Behavior |
|---|---|
all (default) | Started by spky dev and deployed to the cloud. |
devOnly | Local-only, started by spky dev, never deployed. Validation is relaxed (no spec/method/deploy required), so it’s handy for a local sidecar. |
cloudOnly | Deployed to the cloud but skipped by spky dev. |
type: docker: run a prebuilt image
A docker app runs a prebuilt image directly (no Dockerfile build), useful for local infra like a LiveKit SFU, a mock service, or a cache. Fields:
image(required): the image to run, e.g.livekit/livekit-server:latest.ports: published ports. A bare port (7880) maps host→container 1:1 (7880:7880); use"host:container"to remap and a/protosuffix for UDP ("7882/udp").args: appended after the image as the container command (e.g.["--dev"], or["go", "run", "."]to run a service from source).volumes: bind/volume mounts (docker run -v), e.g.["/var/run/docker.sock:/var/run/docker.sock", "${PROJECT_DIR}/../..:/src", "gomod:/go"].${PROJECT_DIR}(the absolute directory ofsp00ky.yml) is expanded in the host portion.workdir: working directory inside the container (docker run -w).dependsOn: names of otherdockerapps that must be ready before this one starts.spky devlaunches apps in dependency order; an unknown name, a self-dependency, or a cycle is rejected at config load (caught byspky lint/spky doctor).healthcheck: an HTTP path (e.g./health) polled on the app’s first published host port until it returns 200. AdependsOnwaits for this, i.e. for the service to be genuinely up, not just for the container to have started. Without it, a dependency is “ready” as soon as its container is running.
Under spky dev it runs as the container sp00ky-dev-<name> on the dev network (reachable from sibling apps by its name) with --rm teardown. ${PROJECT_DIR} is also expanded in env values, and user env overrides the auto-injected SPKY_* vars. With scope left as all/cloudOnly, deploys pull the image and ship it through the same image pipeline as a backend; pair it with scope: devOnly for a purely local service (dependsOn/healthcheck are spky dev-only). env (below) is passed through as container environment variables.
For per-app environment variables (env: { dev, cloud, vault }), see Environment Variables.
Split a service’s config into its own file
An app entry can also be a { path } reference instead of an inline block. path points at a directory containing a sp00ky.app.yml file, so a backend service can keep its own config next to its code:
# sp00ky.yml (project root)
apps:
web:
type: frontend
# ...
api:
path: ./api # pulls ./api/sp00ky.app.yml in as this app
scope: cloudOnly # fields set here override the referenced file
# ./api/sp00ky.app.yml
# yaml-language-server: $schema=https://sp00ky.cloud/schema/app.schema.json
type: backend
spec: ./openapi.yml # paths are relative to THIS file's directory
method:
type: outbox
table: job
schema: ./src/outbox/api.surql
deploy:
dockerfile: ./Dockerfile
port: 3660
The referenced file holds a single app config (the same fields you’d inline under apps.<name>) and validates against its own app.schema.json. Its relative paths are resolved from the service directory, and at load time the whole thing is merged into one effective config. Any field you also set alongside path in the root file wins (main overrides sub). The split is entirely optional, leave your apps inline and nothing changes.
Retention
Finished jobs and run history are deleted automatically, asymmetrically by outcome: a success is read once if at all, a failure is what you went looking for.
| Field | Type | Default | Purpose |
|---|---|---|---|
success | duration | 6h | Job rows in success. |
failed | duration | 14d | Job rows in failed. |
runSuccess | duration | 24h | Schedule/workflow runs in success, skipped, replaced. |
runFailed | duration | 30d | Schedule/workflow runs in failed, killed. |
maxRows | integer | 0 (off) | Hard ceiling on rows in a successful status, per table. Trims oldest-first and never touches failures. A valve for very wide fan-out; the age windows above are the main mechanism. |
mode | all | failures-only | all | Default history mode for every schedule: failures-only means a successful execution leaves nothing but a counter. A schedule’s own history: overrides it either way. Resolved at deploy time, so changing it needs a redeploy — unlike the windows above. Never applies to client db.run() jobs. |
Whatever retention deletes is first folded into permanent hourly counters (_00_run_rollup), so totals survive the rows; spky schedules get shows them. pending and processing jobs, and running runs, are never pruned at any age. An individual schedule can override the run-history windows with history: (see Schedules and workflows). Full behaviour in Jobs → Retention.
Schedules and workflows
schedules and workflows declare work that runs server-side on a clock, whether or not a client is connected. Each cycle the scheduler creates a new job row in the target backend’s outbox table, so a scheduled run is an ordinary job with the usual retry, kill, and recovery behaviour.
A schedule sets exactly one of cron (a 5-field expression, evaluated in timezone, default UTC) or every (a fixed interval, minimum 10s). forEach fans one schedule out into one job per row, and concurrency decides what a fire does when that row’s previous run is still going: skip (default) records the suppressed tick, allow permits overlap, replace kills the in-flight run first.
A schedule may also set history: to override the project’s retention for its own run history — worth doing for a wide fan-out, which writes one run row per item per fire. It takes either the shorthand history: failures-only (a successful run then never persists at all), or an object with mode, success and failed.
forEach.key must be unique per row. The run id is derived from it, so two rows producing the same key collide and only the first spawns; a key naming a field the rows don’t have collapses the whole fan-out onto one run. The scheduler records this on the schedule, visible in spky schedules list.
A workflow is a DAG: steps with no dependsOn are roots and run in parallel, a step with several dependencies is a fan-in join, and each step receives its dependencies’ response bodies under payload.steps. See Schedules and Workflows for the full behaviour and the spky schedules / spky workflows commands.
Either section can also live in its own file, per entry or wholesale, useful once you have more than a handful:
spky lint parses every cron expression, checks each interval, resolves each backend and route against that app’s OpenAPI spec, and rejects a workflow whose dependsOn edges form a cycle, so a definition that lints is one the scheduler can actually run.
Client types
clientTypes is a list of codegen targets; spky generate (run with no args) regenerates every entry from your schema. Each entry has a format and an output path (relative to sp00ky.yml):
typescript: the CLI’s built-in generator emits a typed schema module (e.g.schema.gen.ts).dart: runs spooky_core’s richer generator (dart run spooky_core:spooky_gen), which emits the typed client,Patchclasses, andspookySchema/surqlSchema. Because that tool resolvesspooky_corefrom the enclosing Dart package, it runs from the package directory, auto-derived fromoutput’s nearest ancestorpubspec.yaml. Set the optionalworkdironly to override that. (This replaces a manualdart run spooky_core:spooky_gen/ Makefile step.)
A project can list several outputs, e.g. a TypeScript web client plus one or more Dart clients (a Flutter app, a renderer), and keep them all in sync from one command.
Per-environment overrides
version, logLevel, and apps.<name>.env each accept a {dev, cloud} split so your local stack can differ from what runs in Sp00ky Cloud. A common pattern is to run host-built binaries during spky dev and a pinned image tag in the cloud: