Lists
Build Lists, a black-and-white nested-lists PWA with Goauth login
Summary
Built a React + Vite PWA with a Go (chi, sqlc, pgx) backend on PostgreSQL in a new lists schema. Users log in through Goauth, see their five most recently edited lists, search with a typo-tolerant autocomplete, and create lists and entries from a chat-style bar pinned to the bottom of the screen. Entries can be edited in place, deleted, or turned into nested lists; list rename and delete are guarded by double taps, and list deletion goes through a full-page confirmation naming the directly nested lists. Added a single-image Docker deployment that embeds the built frontend in the Go binary, hardened session handling for PWAs, and published the project as a public GitHub repository. Backend behaviour and session handling were tested against a fake Goauth; the later UI changes were build-checked but not exercised in a browser, and nothing was tested against the real Goauth or on a phone.
Decisions
- Modelled lists and entries as one self-referencing items table, so an entry becomes a list by flipping a flag and nesting is unlimited
- Verified access tokens by calling Goauth’s /auth/me with a one-minute cache instead of sharing the JWT secret
- Proxied /api/auth to Goauth from the Go server and rewrote the refresh cookie path, keeping the browser on one origin for the SameSite=Strict cookie
- Gave the app a dedicated lists_app database role that owns only the lists schema; role, schema and pg_trgm are provisioned by an admin, not by the server
- Served search through pg_trgm word similarity plus substring match, backed by a partial GIN trigram index, with the similarity threshold lowered to 0.4
- Serialised refresh-token use across tabs with a Web Lock and shared tokens over a BroadcastChannel, and treated only explicit Goauth rejections as logout
- Kept the service worker to precaching the app shell with no runtime routes, so API and auth calls always hit the network
- Mirrored Pennywise’s deployment: frontend embedded behind a prod build tag, env injected at runtime through compose env_file
Learnings
- CREATE SCHEMA IF NOT EXISTS still needs CREATE on the database even when the schema exists, so it cannot live in a migration run by a schema-only role
- Single-use rotating refresh tokens make concurrent refreshes from two tabs look like a logout unless refreshes are coordinated browser-wide
- A failed refresh caused by being offline or a 5xx must not be treated as a signed-out session in a PWA
- Go’s built-in MIME table has no .webmanifest entry and Alpine ships no mime.types, so the manifest is served as text/plain unless the type is registered
- go run does not hot reload; air with a small config pointing at ./cmd/server covers it