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.

The Spikefrost Team1 Aug 20266 min read

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.

Each declaration file provisions a resource and binds it into your Worker. The file is the desired state; deploy reconciles reality to match.

Provisioned on deploy

app/

durable-objects.json

queues.json

search.json

email.json

DO class + migration

queue + DLQ twin

AI Search instance

managed sender + address

env.CHAT_ROOM

env.QUEUE_ORDERS

env.SEARCH_DOCS

env.EMAIL_DEFAULT

Each declaration file provisions a resource and binds it into your Worker. The file is the desired state; deploy reconciles reality to match.

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 defineEventsObservability
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.