The stack
What this is built with
Every tool behind eats: what it is, why it's here rather than something else, and where it shows up in the code. 50 of them.
The list lives in backend/web/stack.go. Adding something you've
learned is adding an entry — a test checks that every path still exists, so the page can't quietly
go stale.
Database
One database, owned by the modules that write to it. Nothing else talks to it directly.
-
The relational database everything is stored in.
Why here: One database, strong types, real transactions, and JSON columns where a value is genuinely a document - like an address.
docker-compose.yaml
Transactions are the feature to learn first. Almost every "we need a queue" problem at this size is a transaction plus a table.
-
Numbered .sql files that build the schema, applied in order at startup.
Why here: The schema is code, in git, and the same everywhere. Nobody changes a table by hand.
backend/orders/adapters/db/migrations
Never edit a migration that has run somewhere. Add the next number instead - editing one is how two machines end up with different tables.
-
sqlc
docs →Reads your SQL and generates Go functions with typed arguments and results.
Why here: The queries stay real SQL you can paste into psql, and the compiler checks the Go around them. No ORM guessing what you meant.
backend/orders/adapters/db/queries
sqlc.yaml maps database types to ours - money to decimal, uuid to our own UUID - so the domain types reach all the way down to the row.
-
The PostgreSQL driver and connection pool.
Why here: Talks Postgres's own protocol rather than going through database/sql, and knows Postgres types - arrays, json, timestamptz.
backend/cmd/main.go
It hands json columns to Scan as []byte, not string. Assuming string is exactly the bug that broke every address in this repo - see the README.
Backend
One Go binary. It serves the API, the pages and the static files, and nothing else runs beside it.
-
The language everything on the server is written in.
Why here: Fast to start, one binary to ship, a standard library that covers HTTP and HTML, and a compiler that catches most of what tests would.
go.mod
The standard library goes further than people expect. Reach for a dependency after trying net/http, html/template and log/slog, not before.
-
The HTTP router and middleware layer.
Why here: Routing with path parameters, middleware, and error handling in one place. The course picked it; it does the job.
backend/svc.go
Middleware order is the thing to get right: this repo puts an error-to-HTML middleware on page routes so a failure never answers a browser with JSON.
-
OpenAPI
docs →A YAML description of every endpoint: paths, headers, request and response shapes.
Why here: It's the contract. The server, the client and the API reference are all generated from it, so none of the three can drift.
backend/orders/api/http/openapi.yaml
Write the spec first, then generate. Changing the handler and updating the spec afterwards is how a spec becomes fiction.
-
Generates Go server interfaces and a Go client from the OpenAPI file.
Why here: The strict server makes the compiler refuse a handler that doesn't match the spec, and the generated client is what the web module uses to call the API.
backend/orders/api/http/client
Never edit a *.gen.go file. Change the spec and regenerate - the header on those files says the same thing.
-
Exact decimal numbers.
Why here: Money. float64 can't hold 0.1, and prices that are a penny out are worse than prices that fail loudly.
backend/web/money.go
Keep amounts as decimal from the database to the template. The moment one becomes a float, the rounding is already wrong.
-
Generates and parses UUIDs.
Why here: Every entity has one. They can be made anywhere without asking the database for the next number.
backend/common/uuid.go
These are UUIDv7: they start with the time they were made, so they sort by age - and shortening one to its first characters gives every order the same reference. Take the end.
-
log/slog
docs →The standard library's structured logger.
Why here: Log lines are key and value, not a sentence, so they can be searched once there are thousands.
backend/common/log
Log the correlation id on everything. Following one request through a log is the difference between a five-minute bug and an afternoon.
-
A slog handler that prints colourful, readable lines in a terminal.
Why here: Machine-readable JSON is right in production and unreadable while you're building. This is the development half.
backend/common/log
-
Retries with growing gaps between attempts.
Why here: Things that talk to other machines fail for a second and then work. Retrying immediately, forever, is how one slow service takes down two.
Web UI
Pages rendered by the same binary. No Node, no bundler, no second deploy.
-
html/template
docs →Go's HTML templating, in the standard library.
Why here: The server renders the page. It escapes what it interpolates, so a restaurant called "<script>" is text, not code.
backend/web/templates
Templates should be dumb. Decide things in Go - which pile an order is in, which button it gets - and let the template draw what it's handed.
-
go:embed
docs →Builds files into the binary at compile time.
Why here: Templates, CSS, JavaScript and the API specs all ship inside the one binary. Nothing to copy next to it, nothing to get out of step.
backend/web/render.go
It can only reach files inside its own directory - never a parent. That's why the OpenAPI specs are copied into the web module instead of read from the course's folder.
-
A small script that lets HTML elements make requests and swap the answer into the page.
Why here: The interactive parts - placing an order, accepting one - update in place, with the server still rendering the HTML. No client-side framework, no second copy of the rules.
backend/web/static/htmx.min.js
Every htmx endpoint must answer errors with HTML meant to be shown. A bare 500 with an empty body swaps nothing, and the visitor sees a button that did nothing.
-
Utility classes - one class per thing, written in the HTML.
Why here: The stylesheet is generated from the classes actually used, so it stays small, and there's no growing pile of CSS nobody dares delete.
backend/web/styles/app.css
It only generates classes it can find written out in full. "text-" plus a variable produces a class that never exists - a silent nothing, not an error.
-
Renders an OpenAPI file as a browsable reference with a request client.
Why here: The API documents itself at /app/api, from the same spec the server is generated from.
backend/web/apidocs.go
Several of its defaults call home - telemetry, fonts, an AI agent. Each one is switched off explicitly here, with a test that fails if one comes back.
-
IBM Plex
docs →The typeface, served by Google Fonts.
Why here: A mono and a sans that belong together, free, and distinctive enough that the demo doesn't look like a template.
backend/web/templates/layout.html
It's the one thing these pages fetch from someone else's server. Self-host it when the platform goes anywhere real.
How the code is arranged
Not libraries - decisions. These are the ones that shape where a new file goes.
-
Modular monolith
docs →Several modules, each with its own tables and API, in one binary.
Why here: The boundaries of microservices without the network, the deploys and the debugging. Split one out later if it earns it.
backend/svc.go
The rule that makes it work: a module never imports another module's internals. Break that once and you have a distributed monolith without the distribution.
-
Ports and adapters
docs →The domain in the middle; the database, HTTP and payments at the edges, behind interfaces.
Why here: The rules about orders don't know what a table or a request is, so they can be read - and tested - without either.
backend/orders/app
The direction is the whole point: app defines the interface, adapters implement it. If app imports an adapter, it's inside out.
-
Read models
docs →Queries shaped for a screen, separate from the types the domain writes.
Why here: A courier's list and a kitchen's list are different questions about the same order. Neither has to bend the other's shape.
backend/orders/adapters/db/read_model.go
Build these structs with field names, not by position. An unkeyed literal put a confirmed order's time in the picked-up slot here, and it compiled.
-
Dependencies passed in
docs →The binary builds payments, the database and the rest, then hands them to the modules.
Why here: Swapping the real payment gateway for a demo one is a different line in main.go, not a change inside the module.
backend/svc.go
A module that builds its own dependencies can't be tested without them. That's what made removing the course's payment gateway a one-file change.
-
Generated code is not yours
docs →Files ending .gen.go, static/app.css and the copies in specs/ are all produced by a command.
Why here: Editing one works until the next generate, and then the change is gone with no sign of why.
backend/orders/api/http/openapi.gen.go
Change the source - the spec, the SQL, the template's classes - and run the task. Commit the result so a clone builds without the tools.
Testing
Three sizes, each with a different reason to exist. All of them run with go test.
-
Unit tests
docs →One package, no database, milliseconds.
Why here: Where the fiddly logic lives - statuses from timestamps, money formatting, references from UUIDs.
backend/web/order_status_test.go
Keep them able to run with no environment at all. The moment a test in a package needs Postgres, every test in that package does.
-
Page tests
docs →A real server on a real database, driven with an HTTP client like a browser.
Why here: They catch what unit tests can't: a route that isn't registered, a template that panics, a persona that can open another role's page.
backend/web/webtest
They share one database and run in parallel, so each test makes its own restaurant and looks for its own order. Counting rows on a shared board is a flake waiting to happen.
-
Component tests
docs →The course's own tests, driving the whole service through its API.
Why here: They're the contract the course exercises are graded on. Keeping them passing is how you know a change didn't break the platform underneath.
backend/tests
-
require and assert - readable checks with useful failure messages.
Why here: require.Equal prints both values and stops the test. Hand-written if-blocks print what you remembered to print.
backend/web/webtest/pages_test.go
-
Plausible fake names, addresses and phone numbers.
Why here: Demo customers and couriers get names a person can read out, without inventing a list.
backend/web/handlers.go
Random data in tests cuts both ways: this repo's component tests fail about one run in six because the generator picks a country with no currency.
-
Browser checks
docs →Opening the pages and pressing the buttons.
Why here: Go tests can't run JavaScript. Everything htmx does - the swap, the polling, a script that quietly mounts twice - is only visible in a browser.
Two real bugs here were found this way and nowhere else. When something depends on JavaScript, look at it before believing the tests.
Development
What's on your machine while you work.
-
Task
docs →A task runner: short names for the long commands.
Why here: task css beats remembering the Tailwind invocation. Written in YAML, and it can skip work that's already done.
Taskfile.yml
The UI's tasks live in backend/web/Taskfile.yml, away from the course's file, so a future exercise replacing the root one can't take them with it.
-
Watches files and restarts the server when one changes.
Why here: Save a Go file, the container rebuilds and restarts. No manual restart loop.
backend/reflex.conf
Templates and CSS aren't in its watch list - only .go, .mod and .sql. For those, task css-watch and a browser reload.
-
Docker Compose
docs →Starts Postgres, and the backend, from one file.
Why here: Everyone gets the same database version without installing Postgres. docker compose up and it's there.
docker-compose.yaml
down -v deletes the data along with the container. down keeps it. The v has caught everyone at least once.
-
gofmt and gofumpt
docs →Format Go code. gofumpt is gofmt with a few more opinions.
Why here: Nobody argues about layout, and diffs only contain real changes.
Taskfile.yml
-
go vet and golangci-lint
docs →Find mistakes the compiler allows.
Why here: vet is in the toolchain and catches real bugs - printf arguments, unkeyed struct literals. golangci-lint runs a pile of linters at once.
Taskfile.yml
A warning you've decided to live with should be silenced deliberately. Thousands of ignored warnings mean the new one is invisible.
-
git, in small commits
docs →One change per commit, with a message saying why.
Why here: This repository is meant to be read commit by commit. So is any repository, at 3am, when git log is the only explanation left.
The message is for the person doing git blame in a year. What changed is in the diff; why is only in your head until you write it down.
-
pgcli
docs →A psql replacement with autocomplete.
Why here: For looking at what's actually in the tables when a page shows something surprising.
Taskfile.yml
Deployment
How this gets off a laptop. Some of it is built, some of it is decided and waiting.
-
Docker
docs →Packs the app and everything it needs into an image that runs anywhere.
Why here: The server runs the same image you tested, with no Go installed on it.
backend/Dockerfile
The Dockerfile here is the development one - it runs reflex against mounted source. A production image is a multi-stage build ending in a tiny base with one binary in it.
-
Production image
docs →A multi-stage Dockerfile: built with the Go toolchain, shipped without it.
Why here: 69MB against the development image's several hundred, with no compiler, no source and no package manager on the server.
deploy/Dockerfile
It carries the seed command as well as the server, so the demo restaurants can be added on the server without a second image.
-
A web server that sits in front and handles HTTPS.
Why here: It gets the certificate from Let's Encrypt and renews it without being asked. The app never has to know about TLS.
deploy/Caddyfile
Two of them: Caddyfile gets a certificate from Let's Encrypt, Caddyfile.cloudflare uses one Cloudflare issues when its proxy is on. A line in .env picks which, so neither is edited on the server.
-
Compose, on the server
docs →The same tool as in development, describing what runs in production.
Why here: Three containers on one machine is what this needs. Kubernetes is for problems this doesn't have yet.
deploy/compose.yaml
Settings come from a .env that only exists on the server. The file refuses to start rather than default a password - a missing secret should stop a deploy, not quietly weaken it.
-
cloud-init
docs →A file the server reads on its first boot, once.
Why here: The machine sets itself up - Docker, a deploy user, no password logins - instead of being clicked into shape and forgotten.
deploy/cloud-init.yaml
-
GHCR
docs →GitHub's container registry, where the built image is kept.
Why here: It comes with the repository and needs no other account. The server pulls from it with a token that can only read packages.
A private repository means a private package. Give the server the smallest token that works - read:packages, nothing else.
-
A German hosting provider. The smallest shared instance is a couple of vCPUs with a few GB of RAM.
Why here: Cheap, plain and predictable: a Linux machine with an IP. Nothing to learn about a platform before the app runs.
docs/deploy.md is the step by step. Don't trust a price written in a document - plan names and prices change, and theirs have. Billing is hourly, and deleting a server deletes its disk.
-
Describes the servers and DNS records as files, and makes reality match.
Why here: The machine can be rebuilt from the repository instead of from memory.
The state file knows about everything it manages. Keep it somewhere shared and backed up before there's more than one person or machine.
-
GitHub Actions
docs →Runs the tests on every push, then builds the image and deploys it.
Why here: The tests run whether or not you remembered, on a machine that isn't yours, and a deploy is a push rather than a checklist.
.github/workflows/deploy.yml
It uses no third-party actions - checkout, docker and ssh. Every step is one you can run by hand when it breaks, and nobody else's code holds your deploy key.
Cloud and domain
The name, the DNS behind it, and where the static site lives.
-
The domain
docs →kogiant.com, registered and renewed yearly.
Why here: A name of your own. Projects hang off it as subdomains instead of being paths on somebody else's.
Keep WHOIS privacy on, and use a recovery email that isn't on the domain itself - losing the domain would otherwise lock you out of getting it back.
-
Cloudflare DNS
docs →Answers what kogiant.com means with an address.
Why here: Free, fast to change, and it can hide the server's real address behind its own.
Records take effect in seconds but caches hold the old answer for as long as the TTL says. Lower the TTL before a move, not after.
-
Cloudflare Pages
docs →Hosts static sites straight from a GitHub repository.
Why here: The landing page is HTML, CSS and a little JavaScript. Pushing to main publishes it; there's no server to run.
Right for pages that are the same for everyone. The platform needs a real server, which is why the two live apart.
-
GitHub
docs →Where the repositories live, and where anyone you show this will look first.
Why here: Branches, pull requests, history, and the CI that comes with it.
Never commit a token or a password - and if one lands in a commit, revoke it rather than deleting the commit. The history is already elsewhere by then.