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).
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.
File layout
- Default directory:
./vski(pass another as an argument). Every*.yaml/*.ymlfile 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); defaultdefault. 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 pullwrites one file per kind per database:vski/<db>/<kind>.yamlplus top-levelpipelines.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}
Semantics
Sub-block conventions
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.
Rules
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.
Field changes
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.
How apply executes
databases: created when missing (never deleted).- 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/saveand 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. - Graphs, cron, webhooks, secrets, plugin instances: via their CRUD APIs. Plugin config changes restart exactly that instance's process (server-supervised desired state).
- Pipelines last (they may reference collections and plugins). Every pipeline write is validated server-side before anything is applied.
Secrets
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).
Deletion and prune
state: absenton a resource deletes that specific live resource (collections through adrop_tablemigration, which is a soft delete).- By default, live resources not declared in YAML are untouched.
vski apply --pruneadditionally deletes undeclared resources of the kinds that appear in your files (per database), after confirmation. - Databases are never pruned or dropped by IaC.
Convergence and CI
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
Pull (bootstrap from live)
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)
- 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.
- Getting Started
- Command Line
- Infra as Code
- Project Boilerplate
- SDKs
- Authentication
- Databases
- Collections
- Graphs
- Rules and Access Control
- Granular Access Control
- Triggers
- Plugins
- Realtime
- Full Text Search
- Vector Embeddings
- Workflows
- Pipelines
- Cron
- Webhooks
- Migrations
- Replication
- File Sync
- Backup
- Distributed Realtime
- Tutorial: Embeddings
- Tutorial: Graphs
- Recipe: Calendar Timetree
- Tutorial: Plugins