Declaration files
The four JSON files that provision infrastructure on deploy — durable-objects, queues, search and email — with their real schemas, the desired-state semantics that protect you from accidental deletion, and the wrangler.toml trap.
Four JSON files in app/ provision real infrastructure when you deploy. Each one declares what should exist; the platform diffs that against what does exist and reconciles the difference.
Which file do I need?
| I want | File | Binding you get |
|---|---|---|
| Live shared state, per-key ordering, WebSocket rooms | durable-objects.json |
env.<BINDING> |
| Durable async work with retries and a dead-letter queue | queues.json |
env.QUEUE_<NAME> |
| Meaning-based search over content | search.json |
env.SEARCH_<NAME> |
| Transactional email from app code | email.json |
env.EMAIL_<NAME> + env.EMAIL_<NAME>_ADDRESS |
app/durable-objects.json
{
"classes": [{ "binding": "CHAT_ROOM", "class": "ChatRoom" }],
"confirmDelete": ["OldClass"]
}
| Field | Meaning |
|---|---|
binding |
The variable name it's bound as — env.CHAT_ROOM |
class |
The exported class name |
The class must be exported from your worker bundle or the deploy fails with a clear error — that check exists so a declared-but-missing class can't ship as a broken binding. The platform registers it with a SQLite-backed migration. Full guide: Durable Objects.
app/queues.json
{
"queues": [
{
"name": "orders",
"maxConcurrency": 5,
"maxRetries": 5,
"backoff": { "baseSeconds": 10, "maxSeconds": 3600 },
"target": { "kind": "handler" }
}
]
}
| Field | Default | Meaning |
|---|---|---|
name |
— | Lowercase alphanumeric and dashes. order-events binds as env.QUEUE_ORDER_EVENTS |
maxConcurrency |
5 | Messages in flight into the app at once |
maxRetries |
5 | Attempts before the message moves to the DLQ twin |
backoff |
— | Retry delay is baseSeconds × 2^attempt, capped at maxSeconds |
target |
handler | {"kind":"handler"} calls your exported queues.<name>; {"kind":"agent","agent":"OrderProcessor"} wakes an agent instead |
Provisioning creates the queue and a dead-letter twin. Full guide: Queues.
app/search.json
The richest of the four — it provisions a managed AI Search instance for meaning-based retrieval.
{
"instances": [
{
"name": "docs",
"source": "assets",
"prefix": "search/docs/",
"index": "hybrid",
"tokenizer": "porter",
"syncIntervalSeconds": 3600,
"metadata": [{ "field": "category", "type": "text" }]
}
]
}
| Field | Options | Meaning |
|---|---|---|
name |
— | docs binds as env.SEARCH_DOCS |
source |
assets (default) · builtin · web |
Your R2 bucket · pushed through the Items API · crawl a domain this app serves |
prefix |
defaults to search/<name>/ |
R2 key prefix, for source: "assets" |
url |
— | Origin to crawl, for source: "web" — must be a domain this app serves |
index |
hybrid (default) · vector · keyword |
Which retrieval backends to build |
tokenizer |
porter · trigram |
Stemmed, or substring matching for CJK and Thai |
embeddingModel |
— | Omit for the platform default |
syncIntervalSeconds |
— | Scheduled re-sync cadence |
metadata |
max 5 | Filterable facets: { field, type } where type is text, number, boolean or datetime |
Two limits worth designing around: five metadata facets per instance is a hard cap, and retrieval returns a bounded candidate set. AI Search nominates and re-ranks; your database still owns facet counts, sorting and pagination. Building a faceted product listing on the index alone produces counts that are wrong.
app/email.json
{ "senders": [{ "name": "default", "displayName": "Acme Shop" }] }
| Field | Meaning |
|---|---|
name |
default binds as env.EMAIL_DEFAULT and env.EMAIL_DEFAULT_ADDRESS |
displayName |
The friendly From name, used verbatim in the From: header |
The address is deterministic per app, environment and sender, so each environment gets its own automatically. Full guide: Transactional email.
Desired state, and the delete guard
This is the semantic that matters most, and it's the same for all four files:
Each file is the complete desired set for the environment being deployed. The platform diffs it against what exists and reconciles.
Which raises the obvious risk — remove a line, lose the data behind it. So removals are guarded:
| File | confirmDelete |
confirmRecreate |
What deletion destroys |
|---|---|---|---|
durable-objects.json |
✅ | — | Every instance's storage for that class |
queues.json |
✅ | — | The queue's backlog |
search.json |
✅ | ✅ | The index (rebuildable from source) |
email.json |
✅ | — | Nothing permanent — re-adding restores the same address |
To remove something, name it in the same file:
{
"classes": [{ "binding": "CHAT_ROOM", "class": "ChatRoom" }],
"confirmDelete": ["AbandonedRoom"]
}
Two behaviours follow that are worth internalising:
- An absent file means "keep what exists", never "delete everything". That's deliberate partial-deploy safety: a deploy from a tree where the file didn't sync can't wipe your infrastructure.
- Renaming is delete-plus-create. Storage does not follow a name. Renaming a Durable Object class abandons its rooms; renaming a queue abandons its backlog. Renaming only a binding, with the class or name unchanged, is free.
search.json additionally has confirmRecreate, because some field changes are immutable and force a rebuild of the index rather than an in-place edit.
wrangler.toml is not read
The single most common configuration mistake here:
Deploys never read
wrangler.toml. It exists so local tooling works. A[[durable_objects]]or[[queues]]section in it does nothing.
The platform now catches this rather than silently ignoring it — declare queues there and the deploy fails with:
Error: wrangler.toml declares queues, but Spike deploys do not read
wrangler.toml. Declare them in app/queues.json instead, e.g.
{"queues":[{"name":"orders","maxConcurrency":5}]} — the platform
provisions queue + DLQ and binds env.QUEUE_ORDERS on deploy.
Configuration that isn't one of these files
For completeness, since "what config can an app have?" has a wider answer:
| Surface | What it is |
|---|---|
app/migrations/*.sql |
Schema changes, applied in order and tracked by filename — Database |
app/agents/** |
Agents are code, not configuration — How agents work |
app/events.ts |
The event catalog, declared with defineEvents — Observability |
agent-config/ |
Declared config in YAML: app.yaml, connectors.yaml (messenger claims), routines/*.yaml. Reconciled per environment, and non-production routines default to disabled |
app/package.json |
Dependencies and build scripts — the only other JSON the deploy reads |
.spikefrost/project.json |
Binds your local folder to the app. Not deployed configuration |
And things that are not files at all — worker secrets, custom domains, the firewall, analytics opt-in and per-app cache config are managed through the CLI or console rather than declared in the tree. See Domains and secrets.
Next
- App anatomy — where these sit in the project tree.
- Build and deploy — how reconciliation appears in the deployment event stream.
- Environments — why each environment gets its own resources.
Frequently asked questions
Which config files can an app have?
Four that provision infrastructure: app/durable-objects.json, app/queues.json, app/search.json and app/email.json. Plus package.json for dependencies, SQL migrations for schema, and the agent-config/ directory for declared routines and connector claims.
What happens if I delete a file, or an entry from one?
An absent file means 'keep what exists' — deploys never interpret a missing file as 'delete everything'. Removing an entry from a file that is present is a real removal, and because those destroy data the deploy refuses until you acknowledge it in confirmDelete.
Why doesn't my wrangler.toml configuration work?
Because deploys never read wrangler.toml. It exists for local tooling only. Declaring queues or Durable Objects there does nothing — and the deploy now fails with a message pointing you at the file that is read.
Are these per environment?
Yes. Each file describes the desired state for the environment being deployed, and each environment gets its own queues, search instances, email addresses and Durable Object state.
Do I write these by hand?
You can, and an agent building your app will write them for you. Either way they're normal source files: committed, reviewed, and deployed like any other code.