Overview
The maybloom stack is a way of building a small product system as a single monorepo with a schema-first spine. A Protocol Buffers contract defines every type that crosses a boundary. Code generation produces the transport layer in whichever language a service is written in, and the same generated types reach the client. Humans write the three layers in the middle: handlers, stores, and adapters.
It was extracted from a working production system, where it ran as a Fastify backend, an Expo interface, and static Astro sites in one pnpm workspace. This document set generalizes it, describes the Go backend as a peer of the TypeScript one, and defines when to reach for each runtime. The Go path has since been walked twice: by a Go service brought onto the contract from a REST codebase, and by the reference implementation’s own restart as a Go backend. Since October 2026 the bootstrap skill scaffolds the Go backend by default and the TypeScript one on request.
The core and the paths
The stack has a small core and an expanding set of paths.
The core is the contract: every type that crosses a process boundary is defined in Protocol Buffers, served over Connect-RPC, and generated into whichever languages the system speaks. That one decision is what the stack actually is. It means a client knows, from generated code alone, every resource, every RPC, and every field at its disposal when talking to a service. It means a backend and a client evolve in one monorepo, in one change, with wire compatibility governed by written rules instead of memory. And it means the implementation on either side of the wire is genuinely replaceable — Connect has official implementations in several languages and speaks gRPC and gRPC-Web besides, so any of them can serve or consume the same contract.
The paths are the implementations this document set describes in depth: a Go backend on connect-go (the default since September 2026), a TypeScript backend on Fastify, an Expo interface, static Astro sites. They are validated paths — documented because each has been built and run for real, not because it is required. The defaults are defaults of experience, and the set grows like a skill tree: a new language or framework joins the documented stack when someone walks it far enough to write down what works. Until then it is an open path — permitted, supported by the core, just not yet blueprinted. Choosing a runtime covers the validated paths; its last section covers the open ones.
Even inside a validated path, the decisions are not locked. The blueprints record the current best answer — still experimental, revised as the systems they describe meet new constraints and integrate new technologies — and only the core holds still while they move. Expect these documents to keep changing; that is the stack working as intended.
The pipeline
Every feature moves through the same pipeline, and each stage has exactly one home:
proto → schema → adapter → store → handlers → wiring
↓
api / queries / screen
- proto — the contract. Resource messages, request/response envelopes, and the service definition. See Contracts.
- schema — the database definition. Drizzle TypeScript on the Fastify backend, SQL migration files on the Go backend. Both generate the data layer from a single authoritative artifact.
- adapter — pure functions that translate between proto messages and database rows. The only layer that knows both shapes.
- store — business logic and transactions. Stores speak proto types outward and rows inward.
- handlers — one thin function per RPC: read identity from request context, call the store, shape the response. Input validation belongs to the contract (protovalidate) where the path supports it.
- wiring — the service implementation object registered with Connect. The compiler refuses to build until every RPC in the proto has a handler.
- api / queries / screen — the interface mirror: typed client wrappers, React Query hooks, and screens. See Interface.
Principles
-
The contract is the spine. Everything that crosses a process boundary is defined in proto and generated with Buf. Adding a field or an RPC starts in the contract, always.
-
Generated edges, hand-written middle. Codegen owns the layers that are mechanical: wire types, service scaffolding, the SQL data layer. People own the layers that carry judgment: adapters, stores, handlers. Generated code is never committed; it is rebuilt from source on install. That rule is a pnpm instinct with a Go cost (a fresh clone does not compile until the generators run); the Go document states it.
-
One source of truth per layer. Proto files for the wire. One schema artifact for the database. One service definition per backend. Drift is a build failure, never a code-review catch.
-
The compiler is the checklist. The service implementation object is typed exhaustively, so a declared RPC without a handler is a compile error. sqlc and Drizzle type their queries, so a schema change breaks the build before it breaks a request.
-
Boring dependencies, few of them. Fastify plugins and the Node stdlib on one side; the Go stdlib with a short list of focused libraries on the other. Every dependency should be explainable in one sentence.
-
Single-process honesty. The stack targets one box: a homelab, a small VPS, a container behind Caddy. In-memory caches, in-process schedulers, and embedded dev databases are correct choices at this scale, and the docs say so out loud instead of pretending to be a distributed system.
-
Documentation an agent can execute. The stack ships as agent skills that scaffold a new monorepo and add resources end-to-end. The skills, these docs, and the reference implementation are kept consistent on purpose. See Skills.
Monorepo shape
<repo>/
proto/<slug>/ hand-written contracts (the only proto source)
common/v1/ shared value types
resources/v1/ domain resources, no RPCs
service/v1/ request/response envelopes + the app service
third_party/protovalidate/ the vendored buf/validate/validate.proto (a second, lint-ignored buf module)
go/ the Go module (one per repo), the default backend
cmd/<service>/ one main package per binary
internal/ config, server, store, postgres, auth, version packages
gen/ generated Go from buf (gitignored)
db/ migrations/ and queries/, the SQL sources of truth
buf.gen.yaml, generate.go the Go codegen targets, run as `go generate .` from go/
packages/ TypeScript workspace packages
protocol-buffers/ generated TS (gitignored src/, built on install)
backend/ Fastify + Connect-RPC + Drizzle, when the TypeScript backend was chosen instead
interface/ Expo Router + React Query
<name>-site/ Astro sites, one package per site
scripts/go, scripts/golangci-lint the Go toolchain and linter in their pinned images
.github/workflows/ CI, the image build-and-push, the conventional-commit checks
skills/ the agent skills that scaffold and extend the stack
buf.yaml, buf.gen.yaml contract toolchain, at the root
pnpm-workspace.yaml workspace + version catalog
biome.json one linter/formatter for all TypeScript
infra/ compose + reverse proxy for the deploy target
The shape is singular on purpose: one backend, one Go module, one docs site. A repo that grows a second service keeps the contract layer exactly as it is and pays for the split elsewhere — see When one service becomes two.
Naming that the stack treats as vocabulary, everywhere:
- The client package is the interface — the stack’s word for the human side of the system, chosen over “frontend” because the client is not a front to the backend. Projects adopting the stack may keep their own name for it; these docs use interface throughout.
- Backend layers are handler → store → adapter, and files live where the
skills expect them:
src/handlers/,src/core/<resource>/store.ts,src/core/<resource>/adapter.tsin TypeScript;internal/server/,internal/store/in Go. - Resources are PascalCase singular in protos (
Note), camelCase plural in tables and collections (notes).
CI and images
Every scaffold ships its CI, because a repo whose checks arrive later merges its first weeks unchecked. Three workflows:
- CI runs, on each pull request, only the areas it touches: lint
(
buf lintand biome), one job per package (build, typecheck, the interface’s web export, the docs site), and on the Go path the module’s gates (generate,go mod tidy -diff, vet, build, golangci-lint at the version its wrapper script pins, govulncheck, and the tests with-raceagainst a throwaway Postgres). One table in the workflow maps each path to the areas it checks; a change to a root config or a workflow runs everything, and so does a push to main. A singleCI passedjob reports under the same name on every run and fails if anything that ran failed, so it is the one check a repository requires. - Image is called by CI once those pass, for the images the change
feeds. It builds them, starts each and asks it something only a sound
build answers (the backend’s
HealthRPC must report the commit it was built from), and on main pushes that same image, tagged with the short SHA. A SHA tag is never replaced, so a host can roll back by it: a rerun that finds its commit’s tag builds nothing, and one that cannot tell, or finds another commit’s build under the tag, fails. Then one job, run one at a time across the repository, moveslateston every image together, and only if the commit is main’s tip when it looks: a host that followslatestreceives a tested build, the images’latesttags name the same commit, and an older run cannot move them back. When merges land faster than that job runs, GitHub cancels the older waiting one: that run, and itsCI passedcheck, show red on main withlatestcancelled. It is not a failure: its SHA tags were pushed and a newer commit moveslatest. The registry is a repository variable and its credentials are secrets; until all three are set the images are built and tested and nothing is pushed. The registry must require credentials and be served over HTTPS (or run on localhost), as every hosted one is: the tag check asks it only as the pusher, over HTTPS. Where the images run is not the repository’s concern: a deploy pulls a tag. - Conventional commits lints the pull request title and every commit on the branch with commitlint (pinned by the lockfile, through the workspace catalog), in one job. The title is the one that lands on main when the repository squash-merges.
Every action is pinned to a commit SHA with its release in a comment,
because a tag can be moved to other code and a SHA cannot; Dependabot
proposes the new SHAs weekly. Checkouts keep no credentials
(persist-credentials: false), the workflows hold contents: read and
nothing more, and untrusted text (a pull request title, a changed file’s
name) is read through the environment or with workflow commands stopped,
never interpolated into a script.
What the stack is for
A two-person team building a real product with a long life: seasonal software, tended like a garden. It optimizes for a fast dev loop (clone, install, run, no external services), for changes that touch one layer at a time, and for a future where a service can be rewritten in another language without touching the contract or the interface.
If you need a distributed system, multi-region deploys, or a big-team workflow, this stack will fit badly, and that is by design.