LINKS-13: Unify standalone and saas builds into a single runtime-mode binary #50

Merged
David merged 1 commit from feat/unify-standalone-saas-runtime-mode into main 2026-06-07 19:57:26 +02:00
Owner

Replaces the compile-time standalone/saas split with one binary and one OCI image whose deployment mode is resolved at runtime from OIDC_ISSUER (set = hosted OIDC mode, unset = standalone local-JWT mode).

Server

  • Cargo.toml: removed both [[bin]] targets and the standalone/saas features; single rusty-links binary, default = [].
  • config.rs: added Config::hosted() (mirrors OidcConfig::enabled), load all config sections unconditionally, load a plain .env.
  • main.rs: converted every #[cfg] block to a runtime branch on config.hosted() (default-admin bootstrap, page-guard middleware, OIDC RP router merge, landing page, maintenance guard). Maintenance state and the OIDC verifier are always carried in AppState and stay inert in standalone mode.
  • api/mod.rs: create_router mounts the standalone auth/admin routes only when not hosted, and the maintenance webhook (plus the verifier extension) only when hosted. Runtime mount, not runtime 403, so endpoints unavailable in a mode return 404.
  • auth/middleware.rs: merged the two AuthenticatedUser extractors into one impl that dispatches at runtime (JWT bearer in standalone; rl_session cookie or at+jwt bearer in hosted). api/auth.rs: one unified me handler.
  • models/user.rs: password hashing/verification compile unconditionally. scheduler: OIDC session cleanup gated on hosted(). auth/mod.rs: all submodules compile unconditionally.

Client (WASM)

  • /api/health now reports auth_mode ("standalone" | "hosted").
  • app.rs: fetches the mode once via use_resource into a context provider; Login, Setup, and the navbar logout branch on it at runtime. The spinner-until-resolved pattern keeps hydration consistent (SSR and the initial WASM render both see the pending state).
  • ui/http.rs: mode-agnostic; always sends cookies and attaches a bearer token when one is stored, so the same client works in both modes.

Build & CI

  • oci-build/Dockerfile: single dx build --release; dropped BUILD_MODE, the feature/flag indirection, the output symlink, the binary rename, and the CARGO_BUILD_JOBS=nproc/2 split (cargo now uses the full CPU count).
  • build-oci-image.yml: dropped the matrix; builds and pushes one rusty-links image.
  • check.yml: collapsed the two WASM checks into a single cargo check --features web --target wasm32-unknown-unknown.
  • .env.standalone.example, .env.saas.example, and CLAUDE.md updated to describe runtime mode selection (OIDC_ISSUER is the switch). The hosted template now documents the real OIDC_* variables.

Tests

  • New tests/route_surface.rs: DB-free integration tests (lazy pool) asserting /api/health reports the correct auth_mode and the per-mode route surface (hosted serves no local-auth/admin endpoints; standalone serves no maintenance webhook).

Verification

cargo fmt --check, cargo clippy --all-targets -- -D warnings (default features), cargo check --features web --target wasm32-unknown-unknown, cargo build --all-targets, cargo test --lib (6) and cargo test --features server (135 lib + 6 route-surface) all pass. The 10 failing doctests are pre-existing illustrative examples in error.rs/github/scheduler and are not run by CI (cargo test --lib).

Note: the /oauth2/* BFF routes are assembled in main.rs (not api::create_router) and are gated by the same hosted() switch validated by the route-surface tests.

🤖 Generated with Claude Code

## LINKS-13: Unify standalone and saas builds into a single binary with runtime mode selection Replaces the compile-time `standalone`/`saas` split with one binary and one OCI image whose deployment mode is resolved at runtime from `OIDC_ISSUER` (set = hosted OIDC mode, unset = standalone local-JWT mode). ### Server - `Cargo.toml`: removed both `[[bin]]` targets and the `standalone`/`saas` features; single `rusty-links` binary, `default = []`. - `config.rs`: added `Config::hosted()` (mirrors `OidcConfig::enabled`), load all config sections unconditionally, load a plain `.env`. - `main.rs`: converted every `#[cfg]` block to a runtime branch on `config.hosted()` (default-admin bootstrap, page-guard middleware, OIDC RP router merge, landing page, maintenance guard). Maintenance state and the OIDC verifier are always carried in `AppState` and stay inert in standalone mode. - `api/mod.rs`: `create_router` mounts the standalone auth/admin routes only when not hosted, and the maintenance webhook (plus the verifier extension) only when hosted. Runtime mount, not runtime 403, so endpoints unavailable in a mode return 404. - `auth/middleware.rs`: merged the two `AuthenticatedUser` extractors into one impl that dispatches at runtime (JWT bearer in standalone; `rl_session` cookie or `at+jwt` bearer in hosted). `api/auth.rs`: one unified `me` handler. - `models/user.rs`: password hashing/verification compile unconditionally. `scheduler`: OIDC session cleanup gated on `hosted()`. `auth/mod.rs`: all submodules compile unconditionally. ### Client (WASM) - `/api/health` now reports `auth_mode` (`"standalone"` | `"hosted"`). - `app.rs`: fetches the mode once via `use_resource` into a context provider; `Login`, `Setup`, and the navbar logout branch on it at runtime. The spinner-until-resolved pattern keeps hydration consistent (SSR and the initial WASM render both see the pending state). - `ui/http.rs`: mode-agnostic; always sends cookies and attaches a bearer token when one is stored, so the same client works in both modes. ### Build & CI - `oci-build/Dockerfile`: single `dx build --release`; dropped `BUILD_MODE`, the feature/flag indirection, the output symlink, the binary rename, and the `CARGO_BUILD_JOBS=nproc/2` split (cargo now uses the full CPU count). - `build-oci-image.yml`: dropped the matrix; builds and pushes one `rusty-links` image. - `check.yml`: collapsed the two WASM checks into a single `cargo check --features web --target wasm32-unknown-unknown`. - `.env.standalone.example`, `.env.saas.example`, and `CLAUDE.md` updated to describe runtime mode selection (`OIDC_ISSUER` is the switch). The hosted template now documents the real `OIDC_*` variables. ### Tests - New `tests/route_surface.rs`: DB-free integration tests (lazy pool) asserting `/api/health` reports the correct `auth_mode` and the per-mode route surface (hosted serves no local-auth/admin endpoints; standalone serves no maintenance webhook). ### Verification `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings` (default features), `cargo check --features web --target wasm32-unknown-unknown`, `cargo build --all-targets`, `cargo test --lib` (6) and `cargo test --features server` (135 lib + 6 route-surface) all pass. The 10 failing doctests are pre-existing illustrative examples in `error.rs`/`github`/`scheduler` and are not run by CI (`cargo test --lib`). Note: the `/oauth2/*` BFF routes are assembled in `main.rs` (not `api::create_router`) and are gated by the same `hosted()` switch validated by the route-surface tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: unify standalone and saas into one runtime-mode binary
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 26s
Create release / Create release from merged PR (pull_request) Has been skipped
41b4d1cdcf
Replace the compile-time standalone/saas split with a single binary and OCI image whose deployment mode is resolved at runtime from OIDC_ISSUER (set means hosted OIDC mode, unset means standalone local-JWT mode).

Drop the standalone/saas Cargo features and the second [[bin]] target; one rusty-links binary now builds. Add Config::hosted() (mirrors OidcConfig::enabled), load all config sections unconditionally, and load a plain .env.

Convert every cfg-gated code path to a runtime branch on hosted(): main.rs router assembly (default-admin bootstrap, page-guard, OIDC RP merge, landing page, maintenance guard), AppState fields, api route mounting (standalone auth/admin vs hosted webhook), the AuthenticatedUser extractor (one impl dispatching JWT vs cookie/at+jwt), a single me handler, password hashing/verification, and scheduler OIDC session cleanup.

Report the resolved mode in /api/health as auth_mode. The WASM client fetches it once into a context provider and renders the login, setup, and logout experiences per mode at runtime; the spinner-until-resolved pattern keeps hydration consistent. Make the client HTTP layer mode-agnostic: always send cookies and attach a bearer token when one is stored.

Build a single OCI image (drop the matrix, BUILD_MODE, and the CARGO_BUILD_JOBS nproc/2 split) and collapse the two WASM checks in check.yml into one. Update .env.standalone.example, .env.saas.example, and CLAUDE.md to describe runtime mode selection. Add DB-free route-surface integration tests asserting the per-mode 404 surface.

#LINKS-13

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
David merged commit 4b60a9d5ee into main 2026-06-07 19:57:26 +02:00
David deleted branch feat/unify-standalone-saas-runtime-mode 2026-06-07 19:57:26 +02:00
Sign in to join this conversation.
No reviewers
No labels
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
a8n-tools/rusty-links!50
No description provided.