{"slug": "peg-postgres-and-elm-and-go", "title": "PEG: Postgres and Elm and Go", "summary": "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.", "body_md": "A todo list app demonstrating the Postgres + Elm + Go (PEG) stack end to end.\n\nEvery seam in it is generated:\n\n- **DB → Go** generated with[sqlc](https://sqlc.dev)\n- **Go → Elm** generated by`protoc` , i.e. Protobufs.\n- **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\nstrings written by hand on either side.\n\nThe wire is binary protobuf over gRPC-Web.\n\nGenerating 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.\n\nCodebases 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.\n\nNote: 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.\n\nBy 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.\n\nIn 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.\n\nI 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.\n\nAlso, 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?\n\n`packages/proto/peg.proto` is the contract between server and client:\n\n```\nmessage Todo {\n  uint32 id        = 1;\n  string title     = 2;\n  bool   done      = 3;\n  double createdAt = 4;   // Unix milliseconds\n}\n\nservice Todos {\n  rpc List    (ListRequest)    returns (TodoList);\n  rpc Create  (NewTodo)        returns (Todo);\n  rpc SetDone (SetDoneRequest) returns (Todo);\n  rpc Delete  (DeleteRequest)  returns (DeleteResponse);\n}\n```\n\nErrors are gRPC status codes: `invalid_argument` for an empty title, `not_found` for\na missing id. They travel in trailers, so the client shows the message the handler\nwrote.\n\n`connect-go` serves Connect, gRPC and gRPC-Web from one `net/http` handler at\n`/peg.Todos/*`, with no proxy. The browser speaks gRPC-Web. The Connect protocol\nalso accepts JSON, so the binary wire stays debuggable:\n\n```\ncurl -X POST localhost:8080/peg.Todos/Create \\\n  -H 'Content-Type: application/json' -d '{\"title\":\"buy milk\"}'\n# {\"id\":1,\"title\":\"buy milk\",\"createdAt\":1788926311237}\n```\n\nGenerated code is not committed, so the generators are required. First time:\n\n```\nnpm install                                                      # protoc-gen-elm\nnpm run db                                                       # Postgres 18\nexport DATABASE_URL='postgres://postgres:peg@localhost:5432/postgres'\ngo install google.golang.org/protobuf/cmd/protoc-gen-go@latest\ngo install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest\n# plus protoc, sqlc, elm, node from your package manager\n```\n\n`npm run check` names whichever tool is missing, with its install command. Every\nbuild runs it first.\n\n```\nnpm start                       # → http://localhost:8080\n```\n\n| command | what it does | \n|---|---|\n| `npm start` | build, then serve | \n| `npm run build` | regenerate, then build client and server | \n| `npm test` | full endpoint walk against a scratch schema | \n| `npm run generate` | run the four code generators only | \n| `npm run check` | verify the toolchain | \n| `npm run clean` | delete build output and generated code | \n| `npm run db` /`db:rm` | start / remove the Postgres container | \n\n`PORT` and `DATABASE_URL` are the only knobs.\n\nEdit `packages/proto/peg.proto` or `packages/server/sql/schema.sql`, then\n`npm run generate`. Nothing else needs touching.\n\nEverything you would edit lives under `packages/`. Everything at the root is\nmachinery. Generated code lives under a `gen/` directory and is not checked in.\n\n```\npackage.json             every task; `npm run` to list them\nscripts/                 check-tools, gen, build, dist\nbuild/                   all output; build/dist is the deployable client\n\npackages/proto/peg.proto the contract, owned by neither side\n\npackages/server/         everything Go, module peg/server\n  main.go                wiring: db, handler, file server\n  rpc.go                 the four RPC methods, and pbTodo\n  sql/schema.sql         sqlc's input, and applied at startup\n  sql/query.sql          sqlc's input\n  internal/gen/          GENERATED by protoc-gen-go, -connect-go, sqlc\n\npackages/client/         everything Elm/JS\n  index.html             template; dist.sh rewrites the script tag\n  minify.mjs             oxc wrapper\n  src/Main.elm           hand-written\n  gen/Proto/             GENERATED by protoc-gen-elm\n```\n\n`go.mod` lives in `packages/server`, so `go test ./...` runs from there. Or use\n`npm test`.", "url": "https://wpnews.pro/news/peg-postgres-and-elm-and-go", "canonical_source": "https://github.com/ramblingenzyme/peg/tree/main", "published_at": "2026-09-10 00:28:18+00:00", "updated_at": "2026-09-10 00:51:31.084409+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Postgres", "Elm", "Go", "sqlc", "protoc", "connect-go", "protoc-gen-elm", "gRPC-Web"], "alternates": {"html": "https://wpnews.pro/news/peg-postgres-and-elm-and-go", "markdown": "https://wpnews.pro/news/peg-postgres-and-elm-and-go.md", "text": "https://wpnews.pro/news/peg-postgres-and-elm-and-go.txt", "jsonld": "https://wpnews.pro/news/peg-postgres-and-elm-and-go.jsonld"}}