Skip to content

By Shrin3 min readDraft

Learning Go, Postgres and System Design, before letting AI write it

CanteenCrowd was my final-year college project. Years later, as a senior React developer, I rebuilt its backend from scratch to learn Go, Postgres and system design.

CanteenCrowd started in 2019, as my final-year project in college: an app for running a canteen.

Cut to 2025. I’m a senior React developer, and the frontend is home. What I wanted next was the other side: Go, Postgres, the backend, and the system design that holds it together. Tutorials teach the syntax, but not what happens when real requirements pile up: several customers in one database, roles and permissions, people editing the same record at once.

So I went back to CanteenCrowd: it has all of those problems. I started rebuilding its backend in a new repository, Shrin210-98/CanteenCrowd-go-api, with two rules: step by step, and by hand. No AI assistant, no Claude Code. If I was going to learn it, I was going to write it.

Here’s what I chose, and why.

Postgres: SQL by hand, with sqlc

The easy path in Go is an ORM. I didn’t take it, because the point was to learn Postgres, not to learn how an ORM hides it.

So every query is plain SQL in its own .sql file, and sqlc turns each one into a typed Go function, on top of pgx. I write the SQL; sqlc writes the boilerplate that scans rows into structs. That kept me close to the database while it taught me: how JSONB columns work, what constraints and triggers can enforce that application code can’t, how transactions behave. And I didn’t give up type safety to get there: a query that names a column that doesn’t exist fails when the code is generated, not when it runs.

The standard library’s router

Go’s standard library used to need help with routing, so most projects reached for a router package. Go 1.22 changed that: http.ServeMux learned HTTP methods and path wildcards. Patterns like "POST /login", "PUT /employees/hierarchy" and "GET /ws/chat/{id}" work with nothing but net/http.

So I used it, and kept one less dependency to learn around. The API’s versioned routes are a second ServeMux, mounted under a prefix:

rootRouter.Handle("/api/v1/", http.StripPrefix("/api/v1", v1Router))

Permissions sit next to each route, and a small middleware checks them before the handler runs.

Migrations: golang-migrate first, then Atlas

For migrations I started with golang-migrate: numbered files, an up and a down for every change. It works, and it taught me how migrations are applied and rolled back. But every change was double effort: I wrote the up migration by hand, then the down that undoes it, and had to keep both matching what the database should actually look like.

That’s how I found Atlas. With Atlas I keep the schema as it should be, in plain SQL files, and Atlas compares it with the migration history and writes the next migration itself. It also keeps atlas.sum, a hash of every migration file, so a migration edited after it was applied is caught instead of silently drifting. The commit that switched says it plainly: “Atlas implementation - migrated from golang-migrate”.

What it covers

By the end, the hand-built backend had the parts that make a SaaS a SaaS:

  • Multi-tenancy: many customers in one shared database, each kept apart.
  • Roles and permissions: users, roles and a nested permission tree, with per-user overrides.
  • Sign-in with JWT, carrying the tenant with the user.
  • Staff management: employees, departments, positions and who reports to whom.
  • Live chat over WebSockets.

It needs seven direct dependencies: pgx, golang-jwt, the validator, gorilla/websocket, a UUID library, godotenv and x/crypto, with Docker and a Makefile around it. 58 commits, every line written by hand. It’s still public, and MIT-licensed.

What came next

The hand-built backend did its job: I can explain every decision in it, because I made each one and typed each line. It also gave me a foundation and a set of rules I trusted: SQL in files, generated Go, the standard library where it’s enough, Atlas for migrations.

On that foundation, CanteenCrowd grew into a full product, and that part I built with Claude Code. The next post is about the database side of it: no ORM, and a CI job that won’t let the schema, the SQL and the Go drift apart.

Written by Shrin, who builds CanteenCrowdGitHubLinkedIn