Rewrite the backend in Go #6

Merged
uwayss merged 13 commits from refactor/go-rewrite into main 2026-08-27 09:29:23 +00:00
Owner

Closes #4. Closes #2.

The image was 305 MB, almost all of it the Node runtime. This replaces the Express server with a Go binary that carries the dashboard inside it: 29.8 MB.

The routes, the data/ layout and the JWT are unchanged, so the database, the assets and the tokens already handed out all carry over. Migrations run off PRAGMA user_version, and the first one is a no-op on a database drizzle made. Two dependencies: modernc.org/sqlite because a cgo driver needs a C cross compiler, and golang-jwt because a hand written verifier can miss the algorithm confusion check. The router is net/http.

Checked against the old server

Node on :4000 and Go on :4001, both reading a copy of the production data/. Manifests are byte identical for iOS and Android, headers included. A token Node signed verifies on Go and the other way round. The noUpdateAvailable directive, the old-client 404s, the asset bytes and content types, the traversal blocking and the login limiter all match request for request.

Fixed, not carried over

  • upload_date holds whole seconds, so uploads inside one second tie. The old server kept the wrong updates and served a stale manifest. Every ordering now breaks the tie on the rowid.
  • archive/zip has no zip slip guard, so every entry name is checked to stay below the update directory.
  • MAX_UPLOAD_BYTES caps an upload route that took an archive of any size.
  • CORS is gone. Vite proxies /api in development and the server sends the dashboard in production, so every request was already same origin.

bareed --backup copies the database with VACUUM INTO. The backup script on the server snapshotted it with better-sqlite3 through node, which this image does not carry.

Still to verify

A real device, which is the only thing that proves the protocol end to end. And this PR itself: the runner has no Go, so this is the first run of actions/setup-go.

Closes #4. Closes #2. The image was 305 MB, almost all of it the Node runtime. This replaces the Express server with a Go binary that carries the dashboard inside it: 29.8 MB. The routes, the `data/` layout and the JWT are unchanged, so the database, the assets and the tokens already handed out all carry over. Migrations run off `PRAGMA user_version`, and the first one is a no-op on a database drizzle made. Two dependencies: `modernc.org/sqlite` because a cgo driver needs a C cross compiler, and `golang-jwt` because a hand written verifier can miss the algorithm confusion check. The router is `net/http`. ## Checked against the old server Node on :4000 and Go on :4001, both reading a copy of the production `data/`. Manifests are byte identical for iOS and Android, headers included. A token Node signed verifies on Go and the other way round. The `noUpdateAvailable` directive, the old-client 404s, the asset bytes and content types, the traversal blocking and the login limiter all match request for request. ## Fixed, not carried over - `upload_date` holds whole seconds, so uploads inside one second tie. The old server kept the wrong updates and served a stale manifest. Every ordering now breaks the tie on the rowid. - `archive/zip` has no zip slip guard, so every entry name is checked to stay below the update directory. - `MAX_UPLOAD_BYTES` caps an upload route that took an archive of any size. - CORS is gone. Vite proxies `/api` in development and the server sends the dashboard in production, so every request was already same origin. `bareed --backup` copies the database with `VACUUM INTO`. The backup script on the server snapshotted it with `better-sqlite3` through node, which this image does not carry. ## Still to verify A real device, which is the only thing that proves the protocol end to end. And this PR itself: the runner has no Go, so this is the first run of `actions/setup-go`.
The routes, the data/ layout and the JWT stay as they were, so an existing
deployment keeps its database, its assets and the tokens it handed out.

Two dependencies. modernc.org/sqlite because a cgo driver needs a C cross
compiler and removes the reason to do this. golang-jwt because a hand written
verifier can miss the algorithm confusion check. The router is net/http.

Three faults are corrected rather than carried over:

- upload_date holds whole seconds, so uploads inside one second tie. Ordering
  on the date alone let the retention policy delete the newest update and the
  manifest serve a stale one. Every ordering now breaks the tie on the rowid.
- archive/zip has no zip slip guard, so every entry name is checked to stay
  below the update directory.
- The upload route took an archive of any size. MAX_UPLOAD_BYTES caps it.

CORS is gone. Vite proxies /api in development and the server sends the
dashboard itself in production, so every request was already same origin.

Closes #2.
go:embed reads the dashboard at compile time, so the build has to leave it
where the embed looks. One outDir keeps the two pointing at the same place.

emptyOutDir stops a renamed chunk from staying behind and riding into the
binary. It also removes web/app/.gitkeep, which go:embed needs to compile on a
fresh clone, so that file lives in public/ and the build copies it back.
305 MB to 29.7 MB. Almost all of what goes is the Node runtime and
node_modules.

The dashboard stage and the Go stage both run on the build platform. With
CGO_ENABLED=0 the compiler is the only thing that has to know the target, so
building for amd64 on an arm64 machine emulates nothing.

The runtime user keeps uid 1000. A bind mount keeps the ownership of the host
directory, and the image this replaces ran as node, so an existing ./data
carries over untouched.
pnpm checks now runs gofmt, go vet and go test after the TypeScript tools.
Both workflows filter on the Go paths, and the checks workflow installs the
toolchain that go.mod asks for.

The dashboard and the CLI stay in the pnpm workspace. The CLI is published to
npm and aimed at Expo developers who already have Node.

Closes #4.
feat(backend): copy the database with bareed --backup
All checks were successful
Checks / checks (pull_request) Successful in 2m33s
3dcbb8b6fb
The backup script on the server snapshotted the database by running
better-sqlite3 through node inside the container. This image carries neither,
so the script would fail on its first run after the rewrite.

VACUUM INTO reads the database through SQLite rather than copying the file, so
a write still in the write-ahead log is in the copy and a page the server is
writing cannot be caught half done. The flag runs it through the driver that is
already linked in, and needs no secrets: it reads the database and nothing else.

The log line goes to stderr, so a caller can stream the copy on stdout.
uwayss canceled time tracking 2026-08-26 16:08:37 +00:00
fix(frontend): stop the icon sprite from taking up layout space
All checks were successful
Checks (CLI) / checks (pull_request) Successful in 21s
Checks (server) / checks (pull_request) Successful in 48s
0d6ea95340
uwayss merged commit 3a0bba01f3 into main 2026-08-27 09:29:23 +00:00
uwayss deleted branch refactor/go-rewrite 2026-08-27 09:29:28 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
uwayss/bareed!6
No description provided.