feat(server): atomic first-admin bootstrap, scoped CORS, and deployable web admin - #321
Merged
Merged
Conversation
kafkade
enabled auto-merge (squash)
July 27, 2026 21:22
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.
Description
Makes a self-hosted
ldgrdeployment safe and reachable out of the box, closing the M0/0c release blockers.What's included:
Atomic first-admin election (server). When no
LDGR_ADMIN_EMAILis configured, the first registered user is now elected admin inside a single guardedBEGIN IMMEDIATEcount-and-insert transaction (try_bootstrap_first_admininstorage/mod.rs), so under concurrent sign-ups at most one admin can ever be created. The old fallback counted users and inserted in separate lock acquisitions — a check-then-insert race where two simultaneous registrations could both become admin (and on an unconfigured, internet-exposed instance, whoever registered first silently became admin). The env-seededLDGR_ADMIN_EMAILpath is untouched.Scoped CORS (server). New
LDGR_ALLOWED_ORIGINSconfig (comma-separated allowlist) drives atower_http::corslayer applied inmain.rs. It is empty by default (deny) — no cross-origin browser access unless explicitly opted in — and when set, allows only the methodsGET/POST/PUT/PATCH/DELETE/OPTIONS, theauthorization/content-typeheaders, and no credentials. Needed only for advanced deployments that serve the panel from a different origin than the API.Batteries-included web admin (infra). The existing Caddy container now serves a static build of the Apache-2.0 web app and reverse-proxies
/api/*+/healthto the server, sodocker compose up -dyields a reachable/adminpanel athttp://<host>:8081/adminwith its Server URL pre-filled to the same origin. A newapps/web/Dockerfilebuilds the WASM bundle and the Next.js static export (output: 'export') and packages it intocaddy:2. The AGPLldgr-serverbinary serves zero web assets — Caddy only (ADR-006 / ADR-008 §7). A non-gatingdocker-webimage job publishesghcr.io/<owner>/ldgr-web.Docs.
docs/self-hosting.mdrewritten for the batteries-included deploy (removed the separate-host instructions and the F3 same-origin "Current limitation" caveat).Pre-existing web build fix. The
apps/webstatic export did not build onmain(CI only rannpm test, nevernpm run build). Fixed by targeting the webpack compiler the WASM config requires (next build --webpack) and aligning Tailwind CSS to the v3 toolchain its config is written against. ThewebCI job now runs the WASM + static build so this stays green. Note for reviewers: the Tailwind fix is a v3 pin (matches all existing v3-authored configs, zero CSS churn); a v4 migration is the alternative if preferred.New tests:
tests/bootstrap_race.rs(16 concurrent registers assert exactly one admin; open policy → restuser, invite-only → rest403; plus sequential sanity) andtests/cors.rs(preflight allow/deny + empty-allowlist-disables-CORS).Related Issues
Closes #297
Type of Change
Component
crates/ldgr-core/— Core library (crypto, accounting, storage)crates/ldgr-cli/— CLI toolcrates/ldgr-server/— Sync serverbindings/swift/— UniFFI Swift bindingsapps/ios/— iOS / iPadOS / watchOS appapps/web/— Web app (Next.js + WASM)docs/— DocumentationPrivacy Checklist
Checklist
cargo test --workspace)cargo clippy --workspace -- -D warnings)cargo fmt --check)CI / infra note: No CI job names changed (they are merge gates mirrored in
kafkade/github-infraterraform). Build steps were added to the existingwebjob and a non-gatingdocker-webjob was added indocker-publish.yml— no new required check, so no github-infra terraform change is needed. A compose smoke-test merge gate was intentionally not added.Not verified locally: live
docker compose upend-to-end (no Docker daemon available — validated viadocker compose configon both profiles,caddy validate, and a functional Caddy serve smoke test); web panel styling (no browser).