The AuthSession cookie fix is shipped, but PouchDB longpoll requests
get truncated at ~43s by Traefik on HTTP/2. HTTP/1.1 holds fine;
CouchDB and nginx are clean. Captures the diagnosis trail and the
three options for the next session so the work isn't re-derived.
Commit 40011e2 added manifest.toml to pin transitive deps, but the
Dockerfile only copies gleam.toml — so every Docker build still
re-resolves from scratch and can pick up incompatible versions within
the allowed ranges, defeating the lockfile's purpose.
Coolify's build context and runtime mount paths don't share the repo
tree the way a plain docker compose up does — the bind mount of
./nginx/default.conf failed at container start because the host path
didn't exist. Switch to a build: context: ./nginx pattern (same as
the signup service) so the config is baked into the image at build
time.
Two prior deploy failures came from transitive deps drifting on every
fresh build (gleam_httpc 4.1.1 calling result.then, then glisten 8.0.3
calling list.range). The root cause is no manifest.toml — every build
re-resolved deps from scratch and tripped on broken combinations within
the allowed ranges.
- Bump wisp 1.x → 2.x and mist 4.x → 6.x so the resolver picks
modern, mutually compatible versions of the whole tree
(stdlib 1.0.3, glisten 9.0.1, httpc 5.0.0).
- Commit manifest.toml so deps are pinned by checksum.
- Adapt to wisp 2.x: json_response now takes String, not StringTree.
- Drop unused gleam/http/response import.
- Add signup/build/ to .gitignore.
gleam_httpc 4.1.1 still calls result.then, which was removed from
gleam_stdlib 0.71.0. Bumping the lower bound forces the resolver to
pick 4.2.x or later, which uses result.try. The < 5.0.0 cap is also
widened to < 6.0.0; the public API is unchanged between 4.2 and 5.0.
Long term: committing a manifest.toml lockfile would stop transitive
deps from drifting on every fresh build.
gleam_stdlib now requires >= 1.14.0, breaking the v1.7.0 pin during
build. Bump to v1.16.0 — a stable release with several weeks of bake
time rather than today's v1.17.0.
CouchDB 3.5 derives the Set-Cookie Secure flag from the raw mochiweb
socket scheme and ignores X-Forwarded-Proto/Ssl for cookie attribution.
Behind a TLS-terminating proxy the cookie was emitted without Secure,
so browsers dropped the SameSite=None AuthSession cookie on cross-origin
requests — POST /_session returned 200 but the next GET /_session showed
userCtx.name: null and PouchDB stayed unauthenticated.
Add an nginx sidecar that adds Secure via proxy_cookie_flags, route the
public domain through it, and make couchdb internal. Also persist
same_site=none in couchdb-init so the setting survives container
recreation (local.d/ is not in the data volume).
A small Gleam HTTP server in signup/ exposes POST /signup that validates
a shared INVITE_CODE env var, then PUTs a user document to CouchDB's
_users database using the existing admin credentials. The frontend gets
back 201 + the username and is expected to call /_session itself to
obtain the AuthSession cookie (chosen over server-side cookie forwarding
because vaquant.at and couch.apoena.dev are different sites and the
cross-site cookie path is fragile under modern browser privacy modes).
Wired into docker-compose as a second service, reachable on
signup-couch.apoena.dev with its own Traefik labels and the same
hardcoded UUID network attachment as couchdb. Reuses COUCHDB_USER /
COUCHDB_PASSWORD for the admin call. INVITE_CODE must be set in Coolify
env vars.
PouchDB-style apps call fetch with credentials: 'include' to maintain
the CouchDB _session cookie. Without Access-Control-Allow-Credentials:
true in the preflight response, the browser silently blocks the
mutating request (Firefox HAR shows status 0, _securityState insecure).
Switching cors/credentials to true forces enumerating allowed origins
(wildcard + credentials is forbidden by the CORS spec). Listed are the
common Vite/Next/etc. local dev ports plus the vaquant.at frontend.
Coolify does not interpolate ${...} inside the labels: block, so the
two strings tying this template to its specific Coolify resource cannot
currently be variables. Document where they appear and how to swap
them when redeploying as a new resource.
Coolify is preventing variable interpolation in compose labels — the
container's labels were stored literally as Host(\`\${SERVICE_FQDN_COUCHDB_5984}\`)
etc., so Traefik matched no router and fell back to default_redirect_503.
Variable substitution works in environment: (CouchDB env was correct),
just not in labels: — likely Coolify sets labels via the Docker API
after compose runs, bypassing compose interpolation. Hardcoding the host
and the per-resource network name is the only thing left that works.
The previous diagnostic showed Coolify does not auto-inject Traefik
labels just because SERVICE_FQDN_<NAME>_<PORT> is set — the container
had zero traefik.* labels after deploy, which is why the host returned
503. Adding explicit router+service labels and binding the docker
provider to the per-resource UUID network gives Traefik a definite
backend. Router/service names use the coolcouch prefix so they cannot
clash with any Coolify-managed couchdb router on the same install.
Traefik in this Coolify install resolves backends on a per-resource
network named after the resource UUID, not the shared "coolify" network.
COOLIFY_RESOURCE_UUID is injected into the compose env by Coolify, so
naming the external network from it lets Traefik find couchdb.
The container was only on the project's default network, so Coolify's
Traefik (on the shared coolify network) had no route to it — Traefik
matched the router but reported "no available server". Putting couchdb
on both networks lets Traefik reach 5984 while leaving the init sidecar
talking to it over the default network as before.
When SERVICE_FQDN_<NAME>_<PORT> is set, Coolify auto-injects its own
Traefik router and service labels on the couchdb container. Our manual
labels reused the same router/service name (couchdb), so the two
definitions collided and Traefik ended up with a router whose backing
service had no servers — Traefik returned "no available server" for
couch.apoena.dev. Dropping the manual labels lets Coolify own routing
end-to-end.
The couchdb:3 image purges curl after the build, so the curl-based
healthcheck could never succeed. The cors.ini bind-mount also looks
like the cause of the ~600ms container exit observed in the first two
Coolify deploys. Both pieces are removed; CORS is now applied by the
init sidecar via PUTs against /_node/_local/_config/..., which is
idempotent on redeploy.