Skip to content
spockIt’s only logical !
Non-normative guide. This page shows how to use Spock; the v0 specification governs where they disagree. Spock is pre-1.0 and interfaces may change — see project status.

Seed and disposable state

A Spock program declares its world and populates it in the same file, under the same rules. A seed block is a list of rows written in source; at startup the runtime inserts them through the program’s own contract, exactly as a client would. There is no side door — no SQL dump, no fixtures directory, no loader that bypasses validation. If the seed states something the contract forbids, the program does not start.

That loop is the whole life cycle. The database is rebuilt from source and seed on every load — disposable state is doctrine — so seed replay is the program’s test suite, and there are no migrations. Changing the schema means changing the source and reloading; the world is rebuilt to match, every time, from the one artifact you version-control.

Syntax

A seed block appears at the top level of the file. A program may declare several; they concatenate in source order into one seed, and statements execute in that order. Each statement names a table and provides fields:

table author {
key id: uuid = auto
name: text unique
}
table book {
key id: uuid = auto
author: author on delete cascade
title: text
status: "draft" | "published" = "draft"
}
table review {
key id: uuid = auto
book: book on delete cascade
body: text
rating: int
}
seed {
ursula = author { name: "ursula" }
tove = author { name: "tove" }
dispossessed = book { author: ursula, title: "The Dispossessed", status: "published" }
book { author: tove, title: "The Summer Book" }
review { book: dispossessed, body: "still thinking about it", rating: 5 }
}

The name = table { ... } form is a binding: it captures the inserted row’s key so later statements can fill reference fields with it — author: ursula stores ursula’s generated id. Bindings share one namespace across every seed block in the file, and a binding must be defined before it is used, because statements execute in order and a reference can only point at a row that exists:

table author {
key id: uuid = auto
name: text unique
}
table book {
key id: uuid = auto
author: author
title: text
}
seed {
book { author: ursula, title: "The Left Hand of Darkness" }
ursula = author { name: "ursula" }
}

A binding fills references to the bound row’s table and nothing else: pointing it at a reference with a different target is E025, using it as the value of a non-reference field is E026, and reusing a binding name is E027. The errors reference lists the full E02x family.

Every seed row is a real write

Seed rows pass the same validation as writes arriving over the derived API (v0 specification, §7.3): unknown fields are rejected, values are type- and format-checked, closed-set membership is enforced, defaults apply, references are checked for existence, and unique and key constraints hold. A seed that violates the contract aborts startup with the derived errorauthor_name_taken, say — and the offending seed statement’s source location.

You never have to start a server to find out. spock check is the full load proof: it materializes the schema in an in-memory engine and replays the seed, so a duplicate username or a dangling reference fails the check on your desk, not the deploy. This is what makes seed replay a test suite — every load re-proves that the world you described is a world the contract accepts.

Seed is actorless

Seed runs at startup, before any request, so there is no actor. A field defaulted = me normally stamps the current actor at insert time; in seed there is nobody to stamp, so every required = me field must be named explicitly in the seed row, and omitting one is E022. The tutorial walks into this rule the first time a seeded table carries = me; the default itself, and the whole identity seam, belong to the actor page.

Seed cannot call functions

A seed statement inserts a row; it cannot invoke a fn or mut fn. Functions execute with an actor and may refuse — they are operations, and seed is a statement of facts. When you want seeded state that a function would normally produce, state the resulting rows directly: the instagram example seeds notifications alongside the likes that would have generated them.

Seed assets

A field that references the builtin storage_object table can be seeded from a real file. Given this asset next to the program:

{ "tagline": "a program and its assets are one bundle" }
table document {
key id: uuid = auto
title: text
attachment: storage_object
}
seed {
document { title: "welcome", attachment: file("./welcome.json") }
}

The path is relative to the .spock file and must stay inside its directory — an absolute or escaping path is E050, and file() on a field that does not reference storage_object is E049. At load the runtime reads the bytes, stores them, and materializes a committed storage_object row — name, content type, size, checksum — whose id the field takes; the bytes are then served over the storage plane like any upload.

The program plus its assets are one bundle. The source names the files, the files ride alongside the source, and a missing asset fails replay — at spock check too, not only at start.

The personas pattern

An anchor row seeded with a fixed key is a persona: a known identity to impersonate during development. Write the ids as literals rather than leaving them to = auto, because auto mints a fresh UUID on every replay and every restart is a replay — a persona with an auto id is a moving target for saved requests and client code, while a fixed id survives every restart:

auth table user {
key id: uuid = auto
username: text unique
}
table note {
key id: uuid = auto
owner: user = me
body: text
}
seed {
maya = user { id: "10000000-0000-4000-8000-000000000001", username: "maya" }
luis = user { id: "10000000-0000-4000-8000-000000000002", username: "luis" }
note { owner: maya, body: "welcome" }
note { owner: luis, body: "second voice" }
}

note.owner is = me, so each seeded note names its owner explicitly — the actorless rule above in practice. A running server lists the anchor table’s rows as a picker:

Terminal window
curl -s http://127.0.0.1:4000/~personas
[
{ "actor": "10000000-0000-4000-8000-000000000001", "label": "maya" },
{ "actor": "10000000-0000-4000-8000-000000000002", "label": "luis" }
]

Each entry’s actor is verbatim what goes in the X-Spock-Actor header. Impersonation itself — the header, the persona labels, spock_actor(), and why the seam is deliberately unverified — is the actor page.