Rewrite in Go with templ, HTMX and sqlc on Postgres#39
Open
malpou wants to merge 4 commits into
Open
Conversation
Replaces the SvelteKit/Cloudflare/D1 stack. Behaviour is unchanged: all seven
Playwright spec files pass byte-for-byte untouched, which is what pinned every
decision here.
- Go + templ renders the HTML. HTMX handles the server round-trips (the form
actions keep their ?/{action} URLs); Alpine holds the in-page state that never
needed a round-trip - the preference selector, the submit gate, the inline
edit and expand toggles.
- sqlc compiles internal/db/queries.sql to typed Go over pgx/v5. All app SQL
lives in that one file, as it did in the old data provider.
- messages/{da,en,fr}.json is now read by both sides: the Go server renders from
it, the specs assert against it via Paraglide, so copy cannot drift.
- Go has no CLDR data, so internal/domain/date.go transcribes the exact
Intl.DateTimeFormat output the app produced - including Danish's dot time
separator (kl. 10.00) against en/fr's colon (at 10:00). date_test.go pins
those strings and the DST boundaries.
Only the e2e harness changed, never the specs: db.ts seeds through psql instead
of wrangler, and the database now comes up with the server (scripts/pg.sh)
because Playwright waits for webServer readiness before running globalSetup.
Generated code (templ, sqlc, Paraglide, Tailwind) is committed and marked in
.gitattributes, so a fresh clone builds and tests without the generator
toolchain; CI regenerates and fails if the tree is dirty.
The generated query code lands in internal/db alongside store.go instead of a nested sqlc package, so the store uses sqlc's row types directly and the second set of hand-written structs and their mapping layer are gone. New/NewStore split keeps the two constructors apart. Also drops the running commentary about how the code came to be: the comments now explain what the code does and why, without narrating what it replaced.
pg.sh existed to start Postgres and apply the schema before the server; that is two lines of compose plus two of psql, so it now lives in compose.yaml and the db target where it's visible. CI keeps its own service and sets DB_READY=1 to skip it. gen-types.sh generated two type aliases that are fixed by the data model, out of a sed pipeline over a CHECK constraint. A generator for a constant is machinery with no payoff, so src/lib/types.ts is a plain file again.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the SvelteKit/Cloudflare/D1 stack with Go. Behaviour is unchanged, and
that isn't a claim: all seven Playwright spec files pass byte-for-byte
untouched, which is what pinned nearly every decision in here.
The stack
action in the query string (
?/addOption), andhx-postintercepts thesubmit so a rejected form re-renders in place without changing the URL.
preference selector, the submit gate, the inline edit and expand toggles.
internal/db/queries.sqlinto typed Go over pgx/v5,generated into
internal/dbbesidestore.go, so the store uses thegenerated row types directly and there is no mapping layer.
static/app.css.Worth a look in review
Dates. The Go stdlib ships no CLDR data, so
internal/domain/date.gospellsout the weekday/month names and the per-locale date and time shapes. Two traps
worth knowing about, both pinned in
date_test.go: Danish writes the time witha dot (
kl. 10.00) where en/fr use a colon (at 10:00), and the same UTCinstant renders 10.00 in September but 09.00 in January.
Hidden vs absent. Collapsed notes and respondent names use
x-if, notx-show. Two dashboard specs asserttoHaveCount(0)on collapsed content, whichonly holds if it is absent from the DOM rather than merely hidden - and a
collapsed card genuinely shouldn't ship who answered to the client.
One source of truth for copy.
messages/{da,en,fr}.jsonis read by the Goserver to render and by the specs to assert, so copy cannot drift between the app
and the tests. That is the only reason
src/lib/paraglidestill exists: thespecs import it by that path.
Generated code is committed and marked in
.gitattributes, so a fresh clonebuilds and tests without templ/sqlc/tailwind installed. CI runs
make buildandfails if the tree is dirty afterwards, so it can't rot.
The harness (the only thing touched outside the app)
The specs are untouched; their harness is not.
e2e/db.tskeeps its exactexported API but seeds through
psqlinstead of wrangler.global-setup.tsisgone: Playwright waits for
webServerto be ready before it would run anyglobal setup, so a database started there is too late for the server that needs
it. The database now comes up with the server via
compose.yaml.Deploy note
This is no longer a Worker. It's a single Go binary plus
static/, needingDATABASE_URLand optionallyPORT, sopoll.malpou.ioneeds somewhere thatcan run it. Apply
internal/db/migrations/0001_init.sqlto a fresh databasebefore first boot.