pocketcqrs

command module
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 51 Imported by: 0

README

PocketCQRS

A CQRS + functions-as-a-service backend built on PocketBase as a Go dependency.

  • Write side: commands arrive at a gateway route, are handled by Go Deciders (InitialState / Decide / Evolve), and commit as events to an append-only log — the source of truth.
  • Event store: separate SQLite file events.db alongside PocketBase's data.db, with per-aggregate optimistic concurrency and a global position for catch-up.
  • Read side: projections fold events into ordinary PocketBase collections, so the stock REST API, realtime subscriptions, auth rules and admin UI serve queries unchanged. Rebuild a projection offline with pocketcqrs projection rebuild <name>.
  • Write-guard: direct record writes to guarded collections are rejected for everyone (superusers included); the only writer is the projection engine.
  • Sagas: reactors are durable consumers that map committed events to follow-up commands, dispatched back through the registry with causation/correlation metadata — reactions become events like everything else.
  • Functions (FaaS): user-defined JS functions from pb_functions/ — effects (//@trigger event), HTTP (//@trigger http), cron (//@trigger cron), projections (//@trigger projection + //@schema), full deciders (//@trigger decider), and reactors (//@trigger reactor + //@dispatches) mapping events to follow-up commands — with durability, determinism tiers and read-only query-side bindings per role. Commands and HTTP functions require PocketBase auth by default (--cqrsAllowAnonymous for dev).
  • Calling out: a hard-bounded $http for event/cron functions and reactors, off unless --cqrsAllowOutboundHTTP, restricted to a deployment-wide host allow-list, with the resolved IP re-checked at dial time, no redirects, no retry, a concurrency cap and a body cap. Deciders and projections can never reach the network.
  • Multi-node: single writer, multiple readers. A --cqrsRole=secondary node polls a replicated events.db read-only, runs its own projections, forwards commands and auth traffic to the master (--cqrsMasterAddr), and — with --cqrsVerifyAuth — verifies bearer tokens against the master with a bounded local cache, so authenticated reads work on a secondary without any secret ever leaving the master. See the CLI reference.

extcaller — the external-service-caller

extcaller (package extcaller + cmd/extcaller) is an optional, separately-run component: a consumers.Consumer that matches committed events against configured Rules, calls a third party through a bounded outbound.Client (the same allow-list/timeout/concurrency-cap guardrails as this repo's own $http), and maps the response to follow-up commands dispatched back through the gateway — never appends a raw event, so the decider keeps authority to accept or reject the result. It runs as its own process, reading events.db read-only; it never opens it for writing and never hosts a decider.Registry, so it can never become a second writer.

This is a deliberate exception to "core stays small and reviewable." Everything else non-core — message-bus adapters, out-of-process read-model adapters, alternate transports, GitOps deploy tooling — lives in the separate, private pocketcqrs-extensions repo, kept out of here for exactly that reason. extcaller is the one exception because it's general-purpose enough to be expected by any consumer of this project, not specific to one deployment's opinionated choices — the same reasoning that already put the bounded outbound-HTTP primitive above ($http / outbound.Client) in core rather than in extensions; extcaller is the layer built directly on top of it. Like every non-core component, it is never reachable from a Decide/Evolve call — it's a plain consumers.Consumer, same tier as a reactor, just out-of-process.

go run ./cmd/extcaller \
  -sourceEventsDB /path/to/target/pb_data/events.db \
  -localStore ./extcaller-local.db \
  -gatewayURL http://localhost:8090 \
  -allowedHosts api.example.com

EXTCALLER_GATEWAY_TOKEN must be set (an environment variable, never a flag). extcaller.Config.Gateway is a small interface (just the Dispatch method), not the concrete HTTP client under internal/gatewayclient — that package is unreachable from outside this module by Go's own internal-package rule. Use extcaller.NewGatewayClient(baseURL, token, timeout) for a working extcaller.Gateway, or supply your own implementation. cmd/extcaller/main.go's rules() function is the extension point — it returns nil by default; a real deployment adds its own []extcaller.Rule there, or copies the file as a starting point for its own binary.

Pair it with gateway.Config.ExternalCallerCollection (--cqrsExternalCallerCollection) and a service-account record (see pocketcqrs-extensions' service_accounts/cmd/serviceaccount) so its dispatches get a recognizable "extcall:<name>" actor stamp and carry Causation-Id/Correlation-Id headers, instead of a raw, unlabeled record id.

Status

v0.10.0. Usable and dogfooded; the API is not frozen. Not affiliated with PocketBase; upstream (pocketbase/pocketbase) is an unmodified dependency pinned in go.mod.

It ships empty on purpose. No aggregates, no collections, nothing you did not write — --tutorial opts into the example domains this repo uses to teach and to test itself.

Using this with Claude Code

An agent skill ships with the project — the tiers, the reload loop, and the mistakes that have each cost real time here. Cloned the repo? Claude Code finds .claude/skills/ by itself. Installed the binary instead?

pocketcqrs skill install

Docs

  • Getting started — run it, first command, first function, hot reload
  • Tutorial — a design document to a running slice, with real output including a real collision and how it's handled
  • JS guide — directives, tiers, bindings, determinism, dry-run/dead-letter workflows
  • Go guide — deciders, projections, reactors in Go
  • Ops dashboard — browse the log, operate the barrier, retry dead letters, edit functions
  • Consuming — deployment patterns for frontends, ops tooling and external read-model sinks (with Caddyfiles)
  • Reference: directives · CLI · gateway
  • Domain docs — convention + dogfooded task, order, note (all three need --tutorial)
  • Domain packs — export/import domains, versioning contract, trust model
  • EventModeling import/export — map an eventmodelschema document onto a slice, and back; what round-trips and what does not
  • Contributing
  • Changelog

Development

Requires Go 1.25+.

go run . serve              # empty: no aggregates, no collections but your own
go run . serve --tutorial   # + this repo's example task/order domains

PocketCQRS ships empty, which is what you want for real work — a framework should not create collections you did not ask for. --tutorial opts into the example domains the docs walk through; their JS half lives in examples/pb_functions/ and is copied in rather than loaded from there.

Then open the PocketBase admin UI at http://127.0.0.1:8090/_/.

Install

Two binaries, two install paths (go install <module>@latest covers only the root main package):

go install github.com/jamestryand/pocketcqrs@latest                    # the backend
go install github.com/jamestryand/pocketcqrs/pocketcqrs-dashboard@latest  # the ops dashboard

@latest resolves to the newest semver tag (see the repo's tags; pin an exact version with @vX.Y.Z or a commit hash). The dashboard prints its version with pocketcqrs-dashboard --version.

License

MIT (see LICENSE). PocketBase itself is MIT-licensed, (c) Gani Georgiev.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
Package aggregates holds the write-side models (deciders) of the domain.
Package aggregates holds the write-side models (deciders) of the domain.
Package authforward routes PocketBase's own native auth-collection traffic to the master on a --cqrsRole=secondary node (F-12, the remainder of item 5).
Package authforward routes PocketBase's own native auth-collection traffic to the master on a --cqrsRole=secondary node (F-12, the remainder of item 5).
Package authverify lets a --cqrsRole=secondary node verify bearer tokens it cannot verify itself — the fix for F-13.
Package authverify lets a --cqrsRole=secondary node verify bearer tokens it cannot verify itself — the fix for F-13.
Package batching accumulates decided-but-uncommitted commands into batches and commits each batch's events in one SQLite transaction, instead of one transaction per command (item 4).
Package batching accumulates decided-but-uncommitted commands into batches and commits each batch's events in one SQLite transaction, instead of one transaction per command (item 4).
Package catalog introspects the running platform — aggregates, event types, consumers, collections, functions and reactor flows — into a single document (the catalog), rendered as JSON, Markdown + Mermaid, or domain-doc skeletons.
Package catalog introspects the running platform — aggregates, event types, consumers, collections, functions and reactor flows — into a single document (the catalog), rendered as JSON, Markdown + Mermaid, or domain-doc skeletons.
cmd
extcaller command
Command extcaller is the reference wiring for running an external-service-caller against a pocketcqrs deployment.
Command extcaller is the reference wiring for running an external-service-caller against a pocketcqrs deployment.
Package commandqueue durably records incoming commands before they are decided, so a batching event writer can accumulate many commands' decided events into one SQLite transaction instead of one per command (item 4).
Package commandqueue durably records incoming commands before they are decided, so a batching event writer can accumulate many commands' decided events into one SQLite transaction instead of one per command (item 4).
Package consumers provides the shared checkpointed-consumption engine: named consumers follow the event log in position order with durable checkpoints, so delivery survives restarts.
Package consumers provides the shared checkpointed-consumption engine: named consumers follow the event log in position order with durable checkpoints, so delivery survives restarts.
Package decider implements the functional event-sourcing Decider pattern (see https://thinkbeforecoding.com/post/2021/12/17/functional-event-sourcing-decider): commands are decided against a folded stream state, producing the events that get appended to the event store.
Package decider implements the functional event-sourcing Decider pattern (see https://thinkbeforecoding.com/post/2021/12/17/functional-event-sourcing-decider): commands are decided against a folded stream state, producing the events that get appended to the event store.
Package emschema reads EventModeling documents — the format defined by github.com/jamestryand/eventmodelschema — and maps them onto this project's intermediate domain model.
Package emschema reads EventModeling documents — the format defined by github.com/jamestryand/eventmodelschema — and maps them onto this project's intermediate domain model.
Package events implements the PocketCQRS append-only event store.
Package events implements the PocketCQRS append-only event store.
Package extcaller implements the external-service-caller: a consumers.Consumer that matches committed events against configured rules, calls a third-party REST API through a bounded outbound.Client, and dispatches the response as a follow-up command through a pocketcqrs gateway — never appends a raw event, so the target deployment's decider keeps authority to accept or reject the result.
Package extcaller implements the external-service-caller: a consumers.Consumer that matches committed events against configured rules, calls a third-party REST API through a bounded outbound.Client, and dispatches the response as a follow-up command through a pocketcqrs gateway — never appends a raw event, so the target deployment's decider keeps authority to accept or reject the result.
Package functions provides the PocketCQRS functions-as-a-service layer: user-defined functions triggered by domain events, HTTP requests, or a cron schedule.
Package functions provides the PocketCQRS functions-as-a-service layer: user-defined functions triggered by domain events, HTTP requests, or a cron schedule.
Package gateway exposes the write side over HTTP: commands in, events out.
Package gateway exposes the write side over HTTP: commands in, events out.
Package idempotency lets the command gateway recognize a retried command and replay its original outcome instead of re-deciding it — the fix for F-4 (a client that retries after an ambiguous timeout can double-apply a command).
Package idempotency lets the command gateway recognize a retried command and replay its original outcome instead of re-deciding it — the fix for F-4 (a client that retries after an ambiguous timeout can double-apply a command).
internal
gatewayclient
Package gatewayclient is a small HTTP client for the pocketcqrs command gateway (POST /api/cqrs/{aggregate}/{id}/{command}), for use by out-of-process components in this repo that need to dispatch a follow-up command.
Package gatewayclient is a small HTTP client for the pocketcqrs command gateway (POST /api/cqrs/{aggregate}/{id}/{command}), for use by out-of-process components in this repo that need to dispatch a follow-up command.
localstore
Package localstore wires the two event.Store shapes every out-of-process component in this repo needs: a read-only poll source over the upstream pocketcqrs deployment's events.db, and a separate, locally-writable store for this component's own checkpoints and dead letters.
Package localstore wires the two event.Store shapes every out-of-process component in this repo needs: a read-only poll source over the upstream pocketcqrs deployment's events.db, and a separate, locally-writable store for this component's own checkpoints and dead letters.
Package migrations holds this repo's EXAMPLE PocketBase migrations.
Package migrations holds this repo's EXAMPLE PocketBase migrations.
Package outbound is a hard-bounded HTTP client for the effect and reactor tiers: the one sanctioned way for pocketcqrs to call a third party.
Package outbound is a hard-bounded HTTP client for the effect and reactor tiers: the one sanctioned way for pocketcqrs to call a third party.
Package packs implements domain packs: portable bundles of a domain's function files (pb_functions) plus any plain (non-projection-owned) collection schemas, with a manifest.
Package packs implements domain packs: portable bundles of a domain's function files (pb_functions) plus any plain (non-projection-owned) collection schemas, with a manifest.
Command pocketcqrs-dashboard is the CQRS ops dashboard: a separate, self-contained binary that consumes a pocketcqrs instance over its public API only (never internals, never the event store).
Command pocketcqrs-dashboard is the CQRS ops dashboard: a separate, self-contained binary that consumes a pocketcqrs instance over its public API only (never internals, never the event store).
Package projections folds events from the event store into ordinary PocketBase collections, so the stock PocketBase REST/realtime/auth API serves the query side unchanged.
Package projections folds events from the event store into ordinary PocketBase collections, so the stock PocketBase REST/realtime/auth API serves the query side unchanged.
Package reactors implements cross-aggregate reactions (sagas/process managers): durable consumers that map committed events to new commands, dispatched in-process through the decider registry — so reactions become events like everything else, never out-of-band state changes.
Package reactors implements cross-aggregate reactions (sagas/process managers): durable consumers that map committed events to new commands, dispatched in-process through the decider registry — so reactions become events like everything else, never out-of-band state changes.
Package roles provisions the roles PocketBase collection: the auth collection Item 11's capability-based ops/dashboard access model checks, alongside a genuine PocketBase superuser.
Package roles provisions the roles PocketBase collection: the auth collection Item 11's capability-based ops/dashboard access model checks, alongside a genuine PocketBase superuser.
Package scaffold generates a working vertical slice — a JS decider, JS projections and JS reactors — from a small description of a domain.
Package scaffold generates a working vertical slice — a JS decider, JS projections and JS reactors — from a small description of a domain.
Package users provisions the users PocketBase collection: a genuinely generic, app-level end-user auth collection, distinct from every other collection this project provisions:
Package users provisions the users PocketBase collection: a genuinely generic, app-level end-user auth collection, distinct from every other collection this project provisions:
Package writeguard rejects out-of-band record writes on guarded (projection-owned) collections.
Package writeguard rejects out-of-band record writes on guarded (projection-owned) collections.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL