A small, real-time group-chat system built on Proto.Actor virtual actors (grains) in Go — designed to be read, not just run. blabby is a reference implementation that shows how the grain model maps onto identity-bearing entities (a user, a room), how a stateless gateway bridges plain HTTP/WebSocket clients to a cluster of grains, and how the pieces fit together end to end with a terminal client you can drive in a couple of minutes.
What makes it worth a look:
- Grain-per-entity modelling. Each user and each room is a virtual actor with single-threaded state — no locks, no shared mutable maps. Commands route through a user's own grain to room grains.
- Guided feature tours. Seven documents walk the Proto.Actor features and design tradeoffs the code exercises — actors and grains, supervision, lifecycle and passivation, reentrancy and timers, observability, cluster bootstrap, and roads not taken. Start at the documentation hub.
- A clean client contract. HTTP for commands and queries, a WebSocket for the real-time event stream, JSON on the wire, JWT for identity.
- Decisions written down. Non-obvious choices live in Architecture Decision Records, each with context and consequences.
- Clone-and-run. Generated protobuf code is committed and a terminal client ships in the same module; the only runtime dependency is PostgreSQL, started with a single
docker compose up -d postgres— no broker, no cache.
A stateless gateway (cmd/gateway) fronts a cluster of grains hosted by the backend (cmd/backend): clients speak HTTP and WebSocket to the gateway, and everything behind it is actors. The two are separate binaries — the gateway joins the cluster as a client and hosts one UserConnection actor per socket, the backend joins as a member and hosts the User, Room, and Maintenance grains — so the API tier and the grain tier scale independently. Durable state lives in PostgreSQL; grain memory is a cache over it.
The technical documentation carries the depth: the tier model with its decision records, seven guided feature tours, and the message-flow sequence diagram from login through fan-out to self-healing.
Requirements: Go 1.26 or newer, plus Docker — the backend and gateway read durable room and membership state from PostgreSQL (with the PGroonga extension), so a database must be running.
1. Start PostgreSQL (the postgres service applies the schema and dev seed on its first run; the binaries default to its connection string):
docker compose up -d postgres2. Start the backend (the grain tier — joins the cluster as a member):
go run ./cmd/backend3. In another terminal, start the gateway (HTTP + WebSocket on :8080, joins as a cluster client):
go run ./cmd/gatewayIt defaults to joining a local backend on 127.0.0.1:6330 and logs a one-time loopback advertised-host warning — expected for a same-host run. Override --seeds, --advertised-host, and --cluster-port for a real cluster (details).
4. In a third terminal, start the client:
go run ./cmd/client --server http://localhost:80805. Log in and chat. The client opens a three-pane workspace with a centered sign-in modal. Sign in with one of the built-in development accounts:
| Password | |
|---|---|
alice@example.com |
alice123 |
bob@example.com |
bob123 |
charlie@example.com |
charlie123 |
Type the email address, press tab, type the password, press enter. Then:
- Press
/to open room search, pick a room (generalorrandom), and join it. - Highlight a joined room in the Rooms pane and press
enterto make it active. - Type a message and press
enterto send. Open a second client as another user (or the same one) to watch messages arrive in real time. - Press
ctrl+cto quit.
That's the whole loop — from a fresh clone to exchanging messages in a few minutes across three terminals.
Prefer one command for the server stack? make up collapses steps 1–3: it builds the blabby image on first use and runs docker compose up, starting PostgreSQL, a containerized backend, and a containerized gateway together (plus a local Mailpit for the optional registration-mail flow), streaming all their logs until ctrl+c. Run the client exactly as in step 4. The containers run the built image, not your working tree — rebuild it with make docker after code changes, or use the per-terminal steps above when iterating on the Go code.
The default JWT signing secret is a built-in development value (the gateway logs a warning). Pass
--jwt-secretto the gateway (and--listento change its address) for anything beyond local experimentation; every gateway in a real deployment must share the same secret.
Want to run several gateways and backends that discover each other and route messages across nodes? See docs/multi-node-cluster.md for a runnable walk-through.
Metrics (optional). Pass --metrics to the gateway or --metrics-listen 127.0.0.1:9464 to the backend to expose Proto.Actor's built-in metrics as a Prometheus scrape endpoint; the observability tour covers what you get and how it is wired.
docs/README.md— the hub: architecture at a glance, seven guided tours of the Proto.Actor features this codebase exercises, and a cross-reference from the official Proto.Actor docs pages to the code that exercises each.docs/adr/— Architecture Decision Records: the context, alternatives, and consequences behind the non-obvious choices.api/openapi.yamlandapi/asyncapi.yaml— machine-readable client contracts for HTTP and WebSocket traffic; browse both locally withmake docs-preview.- Implementation details live in the Go docs:
go doc ./..., or browse a package, e.g.go doc ./internal/grain/room.
Common tasks are wrapped in the Makefile:
make spec-lint also requires Node and npx on PATH; the other Quick Start requirements are enough to build, run, and test the Go binaries locally. The database-backed integration tests run only when BLABBY_DATABASE_URL points at the running PostgreSQL — they skip silently without it — so export it before make test for the full suite:
export BLABBY_DATABASE_URL="postgres://blabby:blabby@localhost:5432/blabby?sslmode=disable"make build # compile ./cmd/backend, ./cmd/gateway, and ./cmd/client
make test # race-flagged suite plus the multi-member cluster tests
make up # docker compose up: PostgreSQL + containerized backend and gateway (Quick Start steps 1-3)
make db-reset # recreate the database from a clean volume (wipes local data, reapplies schema + dev seed)
make db-shell # open psql against the running postgres service
make lint # golangci-lint
make spec-lint # validate the OpenAPI and AsyncAPI contracts
make docs-preview # browse both API contracts locally
make diagrams # render docs/*.puml to the committed SVGs (Docker)
make coverage # test coverage report
make generate # buf generateThe protobuf contracts under proto/ generate both message types and Proto.Actor grain scaffolding — interfaces, typed clients, actor wrappers — via buf and protoc-gen-go-grain. The output in gen/ is committed, so cloning and building needs no codegen toolchain. To regenerate after editing .proto files:
go install github.com/asynkron/protoactor-go/protobuf/protoc-gen-go-grain@latest
buf generate && git diff --exit-code gen/ # regenerate, then verify determinismblabby/
├── cmd/
│ ├── backend/ # Grain tier — cluster member hosting the User, Room, and Maintenance grains
│ ├── gateway/ # API tier — HTTP/WebSocket front end; joins the cluster as a client
│ ├── client/ # Terminal (TUI) chat client
│ └── docs-preview/ # Local browser preview for the API contracts
├── internal/
│ ├── grain/ # User, Room, and Maintenance grain implementations
│ ├── actor/ # UserConnection actor — bridges a WebSocket to the User grain
│ ├── gateway/ # HTTP/WebSocket handlers, auth boundary, error envelope
│ ├── clusterboot/ # Cluster assembly: discovery, identity lookup, logging, telemetry wiring
│ ├── persistence/ # PostgreSQL repositories and schema (rooms, membership, messages, accounts)
│ ├── middleware/ # Receiver middleware: structured logging, death-watch translation
│ ├── supervision/ # Logging decorator over the supervisor strategies
│ ├── telemetry/ # Prometheus-backed OpenTelemetry MeterProvider
│ ├── auth/ # Authenticator interface + JWT implementation
│ ├── id/ # UserID / RoomID value types, parsed once at boundaries
│ ├── testutil/ # In-process cluster and grain test harnesses
│ └── ... # plus focused support packages (domain types, error taxonomy, logging setup, snowflake ids)
├── proto/ # Protobuf service + message definitions
├── gen/ # Generated Go from proto (committed — clone and build)
├── api/ # API specs: openapi.yaml (HTTP) + asyncapi.yaml (WebSocket)
└── docs/ # Technical hub, feature tours, rendered diagrams, and ADRs
Writing and small example repositories by the maintainer on protoactor-go's building blocks. The dates matter: the older posts describe the API names of their day (Context.Tell has since become Send), while the mechanics they explain still hold.
- Introduction to golang's actor model implementation (2018) — terminology, concepts, and constructing your first actors.
- How actors communicate with each other (2018) — the messaging methods and what each guarantees.
- How actor.Future works to synchronize concurrent task execution (2018) — futures,
PipeTo, and awaiting inside an actor. - How middleware works to intercept incoming and outgoing messages (2018) — the interception points this repo's logging middleware builds on.
- Use plugins to add behaviors to an actor (2018) — composing reusable capabilities from middleware.
- How proto.actor's clustering works to achieve higher availability (2021) — the clustering model this repo's gateway/backend split rides on.
Focused example repositories, each isolating one mechanism in a few files:
- protoactor-go-sender-example — how an actor resolves the sender to reply to.
- protoactor-go-future-example — future handling patterns.
- protoactor-go-middleware-example — middleware in isolation.
