PEG: Postgres and Elm and Go A developer published a todo-list application demonstrating the Postgres + Elm + Go (PEG) stack in which every seam is machine-generated: sqlc produces the database-to-Go layer, protoc generates the Go-to-Elm bindings, and the API comes from the service block in peg.proto via connect-go and protoc-gen-elm. The author argues that leaning on traditional code generation leaves LLMs less code to generate non-deterministically, and that Go and Elm's strong compile-time typing and Elm's single-direction Model-Update-View architecture give an LLM "less wiggle room to go off the rails" than JavaScript or TypeScript. The wire protocol is binary protobuf over gRPC-Web, served by connect-go from one net/http handler at /peg.Todos/* with no proxy, and generated code is not committed, so protoc, sqlc, elm and node must be installed locally. A todo list app demonstrating the Postgres + Elm + Go PEG stack end to end. Every seam in it is generated: - DB → Go generated with sqlc https://sqlc.dev - Go → Elm generated by protoc , i.e. Protobufs. - The API comes from the service block in peg.proto , via connect-go https://connectrpc.com and protoc-gen-elm . No routing table or URL strings written by hand on either side. The wire is binary protobuf over gRPC-Web. Generating code is now easy and cheap with LLMs, which makes it cheap to explore new languages and see how they work, or get an idea for how they can approach problems differently. Codebases can also get messy very quickly and easily when using LLMs to generate the majority of your code. Go and Elm are both very strongly typed at compile time & are much more opinionated languages than Javascript/Typescript, which hopefully translates to less wiggle room for an LLM to go off the rails. Additionally, the Elm Architecture TEA of a Model state , Update & View is a single direction architecture, and without a JS interop, all changes go through it, whereas in a React application, you can bypass the virtual DOM and directly mutate it yourself. Note: of course Elm compiles down into Javascript, but we're insulated from the madness of the package ecosystem and it doesn't have the escape hatches that Typescript has which allows LLMs to suppress legitimate errors. By leaning on traditional code generation an LLM has less code to generated non-deterministically to build an application. This also lets people & LLMs focus more on the higher level details and increases their leverage/effectiveness when making changes. In this case, we're using dbsql to generate the DB code, and protobufs to define the wire protocol and API. By having the messages and actions API defined by the protobuf definitions, we also couple the frontend and backend together with strong types, which makes it very hard for an LLM to cause them to drift apart and end up in a loop trying to fix it. I chose protobufs because I haven't used them before and wanted to learn, but GraphQL is likely the more appropriate choice, and still has tooling to generate code for servers & clients. Also, I just threw Postgres in there to make the acronym work. I mean, what other DB are you going to use unless you can use SQLite, have very specialised requirements or are already working at a ridiculous scale? packages/proto/peg.proto is the contract between server and client: message Todo { uint32 id = 1; string title = 2; bool done = 3; double createdAt = 4; // Unix milliseconds } service Todos { rpc List ListRequest returns TodoList ; rpc Create NewTodo returns Todo ; rpc SetDone SetDoneRequest returns Todo ; rpc Delete DeleteRequest returns DeleteResponse ; } Errors are gRPC status codes: invalid argument for an empty title, not found for a missing id. They travel in trailers, so the client shows the message the handler wrote. connect-go serves Connect, gRPC and gRPC-Web from one net/http handler at /peg.Todos/ , with no proxy. The browser speaks gRPC-Web. The Connect protocol also accepts JSON, so the binary wire stays debuggable: curl -X POST localhost:8080/peg.Todos/Create \ -H 'Content-Type: application/json' -d '{"title":"buy milk"}' {"id":1,"title":"buy milk","createdAt":1788926311237} Generated code is not committed, so the generators are required. First time: npm install protoc-gen-elm npm run db Postgres 18 export DATABASE URL='postgres://postgres:peg@localhost:5432/postgres' go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest plus protoc, sqlc, elm, node from your package manager npm run check names whichever tool is missing, with its install command. Every build runs it first. npm start → http://localhost:8080 | command | what it does | |---|---| | npm start | build, then serve | | npm run build | regenerate, then build client and server | | npm test | full endpoint walk against a scratch schema | | npm run generate | run the four code generators only | | npm run check | verify the toolchain | | npm run clean | delete build output and generated code | | npm run db / db:rm | start / remove the Postgres container | PORT and DATABASE URL are the only knobs. Edit packages/proto/peg.proto or packages/server/sql/schema.sql , then npm run generate . Nothing else needs touching. Everything you would edit lives under packages/ . Everything at the root is machinery. Generated code lives under a gen/ directory and is not checked in. package.json every task; npm run to list them scripts/ check-tools, gen, build, dist build/ all output; build/dist is the deployable client packages/proto/peg.proto the contract, owned by neither side packages/server/ everything Go, module peg/server main.go wiring: db, handler, file server rpc.go the four RPC methods, and pbTodo sql/schema.sql sqlc's input, and applied at startup sql/query.sql sqlc's input internal/gen/ GENERATED by protoc-gen-go, -connect-go, sqlc packages/client/ everything Elm/JS index.html template; dist.sh rewrites the script tag minify.mjs oxc wrapper src/Main.elm hand-written gen/Proto/ GENERATED by protoc-gen-elm go.mod lives in packages/server , so go test ./... runs from there. Or use npm test .