Back to all posts

Projections: Locally develop, debug, and test with Gaffer

Kurrent Team avatar Kurrent Team
Projections: Locally develop, debug, and test with Gaffer

KurrentDB Projections are server-side JavaScript that turn your events into running totals, derived streams, and read models. They're a powerful feature, but they've also been a frustrating one to develop, largely because they run inside the database, where you have very little control over how you write, run, and inspect them.

Today we're releasing Gaffer, a toolkit that takes the projection runtime out of the database and puts it on your machine, so you can develop, test, and debug projections the way you'd work on any other code.

npm i -g @kurrent/gaffer

The way it works today

If you've been writing projections, you know the routine. To run one, you have to get it onto KurrentDB — usually your dev instance — hand-craft some events, and see what comes back. Deploying it means a curl script you wrote yourself, or pasting the code into the legacy browser UI by hand. None of that is fast, and once your projection lives in a text box in a UI, keeping it under version control becomes an afterthought.

Testing is harder still. Without a local runtime, the only real way to test a projection has been to run it against an actual database. Debugging is its own ordeal: the legacy UI hands off to Chrome's debugger, which is fiddly to set up and tends to bury your handler in unrelated framework code.

Projections end up playing the role that stored procedures used to: critical business logic that lives somewhere you can't easily version, test, or step through.

Gaffer runs the real engine, locally

At the heart of Gaffer is KurrentDB's actual projection engine, lifted out of the database and run on your machine. That's the whole point: what you debug locally is what ships. Because it's the same engine rather than a reimplementation, there's nothing to drift from production, and no "works on my machine" gap to worry about.

Scaffold a projection and run it against a fixture — no database required:

gaffer init                                  # create gaffer.toml in the current directory
gaffer scaffold projections/order-count.js   # add a projection file and register it
gaffer dev order-count --fixture happy       # replay the "happy" fixture through it

All the configuration lives in gaffer.toml — the projections in your project, their entry files, and named fixtures you can re-run by name:

[[projection]]
name = "order-count"
entry = "projections/order-count.js"
engine_version = 2
fixtures.happy = "fixtures/orders.json"

The projection itself is plain JavaScript, the same shape KurrentDB runs:

// projections/order-count.js
fromAll().when({
  $init() {
    return { count: 0, totalCents: 0 };
  },
  OrderPlaced(state, event) {
    state.count += 1;
    state.totalCents += event.body.cents; // event.body is the parsed JSON payload
    return state;
  },
});

And a fixture is just a JSON array of events to feed it:

// fixtures/orders.json
[
  { "eventType": "OrderPlaced", "streamId": "order-1", "data": "{\"cents\": 2999, \"item\": \"Widget\"}" },
  { "eventType": "OrderPlaced", "streamId": "order-2", "data": "{\"cents\": 4999, \"item\": \"Gadget\"}" },
  { "eventType": "OrderShipped", "streamId": "order-1", "data": "{\"trackingId\": \"TRK-001\"}" }
]

gaffer dev replays each event through your projection, printing the state as it evolves and then a final summary — in this case, { "count": 2, "totalCents": 7998 }. The OrderShipped event flows through and is skipped, because there's no handler for it. Each run starts from scratch, so iteration stays fast and deterministic: add a handler, re-run, and see the new state immediately.

A real debugger

Install the KurrentDB Gaffer VS Code extension and you get debug lens directly on gaffer.toml. From there you can set breakpoints in your projection, step in and out, watch expressions, and inspect the call stack, while a side panel shows the state diffed before and after each handler along with the logs and events it emitted. It brings projections the kind of debugging experience the rest of your code already has.

Debug production safely

Point Gaffer at a live KurrentDB instance and it replays real events through your projection locally, and it does so side-effect free: emit and linkTo have no effect on the database, so you can run a projection against production data with no risk of writing anything back.

That makes production incidents much easier to investigate. When a projection misbehaves in production, you can replay the real events on your machine, set a breakpoint, and work out what's happening without ever touching the live system.

Test from your own suite

The @kurrent/projections-testing library runs projections inside vitest, jest, or mocha, on the same engine and without standing up a database — which also means none of the flakiness that usually comes with one:

import { createProjection } from "@kurrent/projections-testing";

const projection = createProjection<{ count: number }>(`
  fromAll().when({
    $init: () => ({ count: 0 }),
    OrderPlaced: (s) => ({ count: s.count + 1 }),
  });
`, { engineVersion: 2 });

for (const { state } of projection.run(events)) {
  // assert on state at each step
}

Projections become ordinary, CI-able, git-versioned code.

Accurate, down to the quirks

Gaffer reproduces KurrentDB projection behaviour exactly, down to the quirks and bugs, so a test that passes locally will pass in production too. On top of that, it statically analyses your projection and flags the gotchas before you hit them: engine quirks it faithfully reproduces (quirk.*), and mistakes in your own code (usage.*) such as an async handler that quietly breaks your state.

Built for AI assistants

gaffer mcp exposes the full suite of tools — scaffold, validate, run, debug, inspect state — to Claude Code, Cursor, Copilot, and any MCP client, with the projection API and worked examples bundled in. Ask your assistant "why isn't this handling OrderShipped?" and let it set a breakpoint and step through.

Summary

Gaffer runs KurrentDB projections locally on the same engine as production, so you can develop, debug, test, and safely replay them like ordinary code. Install it with npm i -g @kurrent/gaffer and give it a try.

References