Why Rezee runs on Go, Bun, and React
The reasoning behind Rezee's polyglot stack - Go for the git engine and CI executor, Bun and ElysiaJS for the API, React with TanStack for the UI - and the seams between them.
Mar 16, 2026 · 4 min read · Kash Gohil
Rezee is one workspace for everything a team ships - and under it, one monorepo with a deliberately split personality: two Go services, a TypeScript API on Bun, and a React front end. Polyglot stacks carry a real coordination cost, so this post is the reasoning: why each language sits where it does, and how the seams stay cheap.
What does the stack actually look like?
One repository, Bun workspaces plus a Go workspace, five deployables:
- denji (Go) - the git engine: smart HTTP and SSH transport over bare repos, plus a JSON metadata API. Its architecture has its own post.
- kobeni (Go) - the CI/CD pipeline executor.
- makima (TypeScript/Bun) - the application API: ElysiaJS, REST, and every authorization decision in the system.
- pochita (React) - the workspace UI: React 19, TanStack Start/Router/Query, Tailwind, and a TipTap-based docs editor.
- The marketing site you're reading - React too, same monorepo, same pipelines.
Postgres 16 underneath, accessed through Drizzle ORM from a shared database package, with shared TypeScript types in a common package between API and UI. One bun test && go test ./... runs the lot.
Why Go for the engines?
The two Go services share a job description: sit in the hot path, move bytes, spawn processes, never surprise anyone. That's Go's exact comfort zone.
The git engine is mostly subprocess plumbing - piping the git binary to sockets with pkt-line framing at the edges. Go's os/exec, real streaming I/O, and single-binary deploys (the whole service has one non-stdlib dependency, golang.org/x/crypto for SSH) make it the shortest path to a boring, auditable server. The CI executor has the same shape - containers, streams, timeouts - where a goroutine per run beats an event loop for clarity.
Could both be TypeScript? Yes, and earlier prototypes leaned that way. The deciding factors were process control (Go's subprocess and signal handling is simply less fussy), deployment (a static binary in an Alpine image with the git CLI is the entire artifact), and the temperament of the language: engines want the least dynamic tool that does the job.
Why Bun and ElysiaJS for the API?
Because the API layer is where product iteration happens - auth, workspaces, issues, docs, chat, pipelines, webhooks - and product iteration wants TypeScript: one language with the front end, one shared type vocabulary, one hiring pool for the whole product surface.
Within TypeScript-land, Bun earns its place with speed in the loop that matters (install, start, test - bun test runs the API suite without a config file) and with being the runtime and the toolchain: no separate bundler, no test framework negotiation. ElysiaJS on top gives typed routes with OpenAPI generation nearly for free, and stays plain REST - the UI talks to it with fetch and a session cookie, no RPC framework in between. Boring transport, typed edges.
The honest trade: Bun is younger than Node, and we occasionally feel it in ecosystem corners. We take that trade for the iteration speed and would again.
Why React and TanStack for the UI?
React 19 with the React Compiler, and TanStack for the load-bearing parts - Router for file-based routing with typed search params, Query for server state. The pattern worth naming: polling over push, until push is earned. Chat and live pipeline views refetch on short intervals through TanStack Query rather than holding WebSockets - a deliberately unclever choice that keeps the API stateless, the failure modes visible, and the door open to upgrade specific surfaces to push when the polling cost actually bites. Boring first; clever when measured.
The editor is TipTap (ProseMirror) with a slash-command block menu - document JSON persisted through debounced saves, the parent owning persistence so the editor stays a pure component.
How do the seams stay cheap?
Polyglot fails when the languages share responsibilities. Ours don't - the boundaries are network contracts with one owner each:
- Authorization lives in one place. The git engine forwards every auth decision to the API over an internal endpoint secured by a shared secret; Go never grows its own permission logic.
- Git bytes never touch TypeScript. Clone and push go client → denji directly; the API only ever handles metadata JSON.
- Events flow one way. Pushes notify the API via a post-receive hook; the API decides what happens next (pipelines, issue updates, webhooks).
Three seams, each a small HTTP contract. The rule that keeps it maintainable: a service may call the API; services don't call each other sideways.
FAQ
Why not one language for everything?
Because the workloads genuinely differ: the engines are stream-and-subprocess programs where Go excels, and the product surface is a types-shared-with-the-UI iteration loop where TypeScript excels. The cost of two languages is real but contained to three small HTTP seams; the cost of forcing either workload into the other's language is paid on every feature.
Why Bun instead of Node?
Speed of the daily loop and consolidation of the toolchain - runtime, package manager, and test runner in one, which matters in a monorepo with several TypeScript workspaces. The ecosystem-maturity trade is real; for a young codebase that sets its own conventions, it's been the right side of the trade.
Why polling instead of WebSockets for live views?
Because polling through the existing query layer costs almost nothing to build, keeps the API stateless, and its staleness window (a couple of seconds) is invisible for chat and CI logs at current scale. Push is an optimization with real operational weight; we'll pay for it per-surface when measurement says to, not on principle.