B · Launching Effectstream + Local Dev

Read the template in 10 minutes

The whole app is five small files plus one service. Read them in this order and the rest of the workshop is just editing them.

▼ details below

The golden path: one mint, traced

Transfer eventERC-721 on Hardhat config.dev.tsprimitive watchesthe contract grammar.tsinput type:"transfer-assets" state-machine.tstransition writesto Postgres api.tsGET /api/…reads Postgres

1 · packages/node/grammar.ts — 7 lines

Every input the app understands. The template ships two, both from builtin grammars:

export const grammar = {
  "transfer-assets": builtinGrammars.evmErc721,        // {to, from, tokenId, isBurn}
  "midnightContractState": builtinGrammars.midnightGeneric,
} as const satisfies GrammarDefinition;

2 · packages/node/config.dev.ts — the wiring

Three nested builders, always in the same order:

3 · packages/node/state-machine.ts — the logic

One stm.addStateTransition(kind, function* (data) {...}) per grammar key. The data object carries parsedInput (typed by the grammar!), blockHeight, signerAddress, and randomGenerator — remember that last one. DB writes go through yield* World.resolve(query, params).

4 · packages/node/api.ts — read-only REST

A Fastify router with direct SQL reads. State changes NEVER come through here — they only come from chains.

5 · packages/database/ — migrations + typed queries

migrations/*.sql create tables (applied by the sync node at startup); sql/*.sql holds named queries that bun run build:pgtypes compiles into typed TypeScript (*.queries.ts) — those are the objects the STM resolves.

6 · packages/batcher/ — the gasless mailbox

Everything above is reading chains. The batcher is how users write without paying gas: it's a small HTTP service (:3334) that accepts signed inputs at POST /send-input, verifies each signature, collects inputs for a time window (1 s in dev), and submits the whole batch as one transaction to the EffectstreamL2 contract — the batcher's wallet pays, and the sync node re-verifies every inner signature when the batch arrives, so the batcher can't forge anyone's moves.

Two things to spot in batcher.dev.ts:

What we'll touch in Section C

FileStepChange
packages/database/migrations/001-game.sql1new: cards, map, battles, season tables
packages/database/sql/game.sql1new: 15 typed queries
packages/node/game.ts2new: rules constants, stats, battle math
packages/node/{grammar,state-machine,api}.ts2replace: game logic
game.html3new: the whole UI, one file
packages/node/config.dev.ts + batcher4add: the L2 primitive that makes moves flow
Checkpoint
Open the files and find: the two grammar keys, the primitive that feeds transfer-assets, the transition that writes to evm_midnight, and the batcher's two adapter targets in batcher.dev.ts. If you can point at all four, you know the codebase.
← First launch1 · Card + map tables →