Skip to content

Infrastructure as Code

VSKI deployments can be managed as code: the desired state of collections, pipelines, schedules, secrets, plugin instances and graphs is declared in YAML files and converged onto a deployment with the vski CLI (vski plan / vski apply / vski pull).

Terminal window
vski plan # diff the ./vski directory against the deployment (exit 2 on drift)
vski apply # apply the diff (asks first; --yes skips)
vski pull # export the live deployment back to YAML

The app template is the reference repository: its vski/ directory declares a collection, a plugin instance, a pipeline and a nightly schedule, and make iac-plan / iac-apply / iac-pull wrap the command line.

  • Default directory: ./vski (pass another as an argument). Every *.yaml / *.yml file under the directory is merged (recursively, in path order).
  • Every file requires version: 1. Unknown keys (at any level) are errors, not warnings: the format is strict so typos never silently deploy half a resource.
  • An optional per-file database: scopes db-bound resources (collections, graphs, plugins); default default. Global kinds (pipelines, cron, webhooks, secrets) are unique across the deployment.
  • Resource names must match [a-zA-Z0-9_-]{1,64}; duplicate declarations across files are errors.
  • vski pull writes one file per kind per database: vski/<db>/<kind>.yaml plus top-level pipelines.yaml, cron.yaml, webhooks.yaml, secrets.yaml, databases.yaml.
version: 1
database: default
collections:
notes:
type: base # base | auth
fields:
- {name: title, type: text, required: true}
- {name: body, type: text}
- {name: mood, type: select, options: {values: [good, bad]}}
- {name: author, type: relation, options: {toCollection: users}}
rules: # null ≡ "" = admin-only; "*" = public
list: "*"
create: null
indexes:
- {name: idx_notes_created, sql: "CREATE INDEX idx_notes_created ON notes(created)"}
views:
- {name: notes_view_recent, sql: "SELECT * FROM notes ORDER BY created DESC"}
fts: {fields: [title, body]} # full-text index over these fields
options: {triggers: []}
pipelines:
note-intake: # the map key is the pipeline name
enabled: true
triggers:
- {type: collection, collection: notes, actions: [create]}
steps:
- {id: enrich, type: plugin, target: hello/echo, params: {message: "{{trigger.record.title}}"}}
output: {echoed: "{{steps.enrich.output.message}}"}
cron: # global
nightly-scan:
schedule: "0 2 * * *"
type: http
url: "pipeline://note-intake" # scheduled pipeline kick
webhooks:
notify:
url: https://example.com/hook
method: POST
headers: {X-Source: vski}
retryLimit: 3
secrets:
openai_key:
fromEnv: OPENAI_API_KEY # value read from the env at apply time
plugins: # plugin instances (db-scoped)
hello:
plugin: hello
enabled: true
config: {greeting: hello}
graphs:
knowledge:
rules: {list: "*"}
options: {directed: true, maxNodes: 100000}
databases: # ensure-exists; never deleted by IaC
analytics: {description: Analytics}

Each collection sub-block is independently managed: omitted means unmanaged (left as is), present is authoritative: an explicit empty indexes: [] removes every index. fields is required when creating a collection.

null and "" are the same thing (admin-only/deny); "*" is public; anything else is a rule expression. Because the migration format cannot express “revert to null”, desired null is applied as "", identical semantics.

Fields are matched by name. New fields are added; removed fields are soft-dropped (column data stays in SQLite). Modifying an existing field (type, required, unique, hidden, default, options) is rejected with an explanation: drop and re-add the field (data loss for that column) or use a custom_sql migration. Auth collections’ system fields (email, passwordHash, tokenKey, verified, roles, data) are never managed.

  1. databases: created when missing (never deleted).
  2. Collections through the migration system: the diff is rendered as an ordinary migration file (same format the dashboard’s generator emits), uploaded via POST /api/migrations/save and applied, so schema changes keep their audit trail, pre-migration snapshots and rollback. --save-migrations <dir> also writes the generated YAML to disk for review artifacts.
  3. Graphs, cron, webhooks, secrets, plugin instances: via their CRUD APIs. Plugin config changes restart exactly that instance’s process (server-supervised desired state).
  4. Pipelines last (they may reference collections and plugins). Every pipeline write is validated server-side before anything is applied.

Secret values never live in YAML. Declare the name and its env source; apply sets the value from the environment when the variable is present. A new secret whose env var is unset is a warning, not drift (so plan can gate CI on convergence in environments that legitimately don’t hold the value). Values are write-only server-side: drift is undetectable, rotation is imperative (vski secrets set).

  • state: absent on a resource deletes that specific live resource (collections through a drop_table migration, which is a soft delete).
  • By default, live resources not declared in YAML are untouched. vski apply --prune additionally deletes undeclared resources of the kinds that appear in your files (per database), after confirmation.
  • Databases are never pruned or dropped by IaC.

apply is convergent: re-running against the same files is a no-op. plan exits 0 only when the deployment matches the desired state. A CI recipe:

# .forgejo/workflows/iac.yaml (excerpt)
- run: vski plan vski # fails the pipeline on drift (exit 2)
# after merge:
- run: vski apply vski --yes

vski pull exports the live state (including rules, indexes, views, FTS fields, pipeline definitions, plugin configs) in the same format apply consumes; the round trip is a no-op by construction (covered by e2e tests). Secret values come out as fromEnv: SECRET_<NAME> placeholders you fill in.

What IaC deliberately does not manage (v1)

Section titled “What IaC deliberately does not manage (v1)”
  • Record/graph data (seeding): use the APIs or plugins.
  • API keys (credentials), replication topology, server env config (.env/compose; see Getting Started).
  • Plugin source code distribution: plugins are baked into distribution images (Plugins); IaC manages their instances.