diff --git a/.derived/codebase-index/by-spec/016-docs-website.json b/.derived/codebase-index/by-spec/016-docs-website.json index 09cb0f3..42a8f5a 100644 --- a/.derived/codebase-index/by-spec/016-docs-website.json +++ b/.derived/codebase-index/by-spec/016-docs-website.json @@ -46,5 +46,5 @@ "specStatus": "approved" }, "schemaVersion": "1.1.0", - "shardHash": "30f447d23ab2ff3b60e3f3fd419dbc2bd3cd34c178e8850ae00ad85f349425e5" + "shardHash": "d4d549831ffb7c8b69ac22d75cb204d9acaf4d3789e3ba8f80d0c9c12b15a616" } diff --git a/.derived/spec-registry/by-spec/016-docs-website.json b/.derived/spec-registry/by-spec/016-docs-website.json index c0483f7..859f9d6 100644 --- a/.derived/spec-registry/by-spec/016-docs-website.json +++ b/.derived/spec-registry/by-spec/016-docs-website.json @@ -34,13 +34,14 @@ "FR-03: Constraint conformance", "FR-04: Inert until Pages is enabled", "4. Acceptance criteria", - "5. Out of scope" + "5. Out of scope", + "Amendments" ], "specPath": "specs/016-docs-website/spec.md", "status": "approved", "summary": "The standalone Docusaurus v3 documentation site under `website/` and the `deploy-docs.yml` workflow that builds it and publishes to GitHub Pages. The site is a shipped deliverable (authored docs about the architecture, security model, governance, and developer workflow), not part of the runnable app or the npm workspace. The publish workflow runs on pushes to `main` that touch the site, is inert until a repo admin enables Pages, and satisfies the SHA-pin (spec 011) and allow-listed-publisher (spec 012) constraints like every other workflow.\n", "title": "Documentation website: the Docusaurus docs site and its GitHub Pages publish workflow" }, - "shardHash": "e316f8516b437c321e09779e09c561c5bdafb313591100faa86be22f9d551029", + "shardHash": "f1ffef0392d6ab24a576a67ac3af29a0823a829e35943ca8af26ec5cd6c2ceef", "specVersion": "1.1.0" } diff --git a/PLACEHOLDERS.md b/PLACEHOLDERS.md index 1fdc267..8434db0 100644 --- a/PLACEHOLDERS.md +++ b/PLACEHOLDERS.md @@ -49,7 +49,6 @@ RAUTHY_ISSUER=https://rauthy.example.com | Placeholder | Description | How to Generate | |-------------|-------------|-----------------| | `{{GENERATE_WITH_OPENSSL_RAND_BASE64_32}}` | Session secret (32+ characters) | `openssl rand -base64 32` | -| `{{REDIS_CONNECTION_STRING}}` | Redis connection URL | `redis://hostname:6379` | ## Files Using Placeholders @@ -74,7 +73,6 @@ Look for values wrapped in `{{...}}`: # apps/api/.env.example RAUTHY_ISSUER={{YOUR_OIDC_ISSUER_URL}} RAUTHY_CLIENT_SECRET={{YOUR_OIDC_CLIENT_SECRET}} -REDIS_URL={{REDIS_CONNECTION_STRING}} ``` ### 2. Replace with Actual Values @@ -85,7 +83,6 @@ Remove the `{{` and `}}` brackets and insert your real values: # apps/api/.env (your actual config) RAUTHY_ISSUER=https://rauthy.example.com RAUTHY_CLIENT_SECRET=s3cr3t-from-rauthy -REDIS_URL=redis://my-redis:6379 ``` ### 3. Verify No Placeholders Remain diff --git a/docker-compose.yml b/docker-compose.yml index 24504f4..e364b96 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -# Encore self-host compose: runs the pre-built Encore image + Postgres + optional Redis. +# Encore self-host compose: runs the pre-built Encore image + Postgres. # The API image is built separately via `npm run build:api` (encore build docker). # Primary local development uses `npm run dev` (encore run + Vite; auto-provisions Postgres). # Use this file for local containerised self-host or staging validation of the built image. @@ -27,20 +27,6 @@ services: networks: - app-network - redis: - image: redis:7-alpine - profiles: - - cache - ports: - - "6379:6379" - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 5s - retries: 5 - networks: - - app-network - # Image built by `npm run build:api` (encore build docker --base apps/api/Dockerfile.base). # The single image serves both the Encore API (port 4000) and the built SPA (web service / api.static). api: @@ -58,8 +44,6 @@ services: GATEWAY_OAUTH_CLIENT_ID: ${GATEWAY_OAUTH_CLIENT_ID:-} GATEWAY_OAUTH_CLIENT_SECRET: ${GATEWAY_OAUTH_CLIENT_SECRET:-} PRIVATE_API_BASE_URL: ${PRIVATE_API_BASE_URL:-} - # Optional: Redis-backed rate limiting (REDIS_URL is NOT a session store) - REDIS_URL: ${REDIS_URL:-} ports: - "4000:4000" depends_on: diff --git a/docker/README.md b/docker/README.md index 35badc7..c35bbb9 100644 --- a/docker/README.md +++ b/docker/README.md @@ -35,7 +35,6 @@ Services and profiles: | Service | Profile | Purpose | |---------|---------|---------| | `postgres` | (default) | SQLDatabase backing (Encore-managed migrations); required by `api`, so always started | -| `redis` | `cache` | Optional Redis-backed rate limiting (`REDIS_URL`); not a session store | | `api` | (default) | The Encore-built image; `depends_on` postgres | Bring up the full stack: @@ -44,10 +43,7 @@ Bring up the full stack: # Build the image first (SPA into apps/api/web/build, then the Encore image) npm run docker:build -# Start Postgres + api + Redis -docker compose --profile cache up -d - -# Or just Postgres + api (Redis is optional) +# Start Postgres + api docker compose up -d ``` diff --git a/docs/AUTH-SETUP.md b/docs/AUTH-SETUP.md index fd077c0..d87f5cf 100644 --- a/docs/AUTH-SETUP.md +++ b/docs/AUTH-SETUP.md @@ -143,7 +143,6 @@ RATE_LIMIT_API_MAX=100 RATE_LIMIT_API_WINDOW_MS=900000 RATE_LIMIT_AUTH_MAX=20 RATE_LIMIT_AUTH_WINDOW_MS=900000 -# REDIS_URL=redis://localhost:6379 # optional: Redis-backed rate limits (NOT sessions) ``` --- diff --git a/docs/analysis/hiqlite-napi-encore-feasibility.md b/docs/analysis/hiqlite-napi-encore-feasibility.md new file mode 100644 index 0000000..452c9a7 --- /dev/null +++ b/docs/analysis/hiqlite-napi-encore-feasibility.md @@ -0,0 +1,212 @@ +# Feasibility: hiqlite-in-Encore.ts via NAPI (native, in-process) + +**Verdict: do not build now. Shape B (native Encore primitive) is structurally +disproportionate; Shape A (standalone napi-rs addon) is technically clean but +solves a value problem that does not yet exist. Recommendation: neither, with +Shape A held in reserve behind a concrete shared-state requirement.** + +Date: 2026-07-05. Spike is strategic/forward-looking; no live gap. + +--- + +## TL;DR reasoning chain + +1. The two-tokio-runtime worry (Q2) is **not** the blocker. hiqlite never + creates its own runtime and pushes blocking I/O onto OS threads; a napi + addon coexists with Encore's Rust runtime as an isolated second tokio in a + separate dylib. Standard, safe. +2. The real wall is **Q3, clustering under horizontal scale**. hiqlite's value + in-process (shared cache / dlock / counters / listen_notify) exists **only** + if replicas form one Raft group. That forces StatefulSet + static membership + + headless-Service DNS into every tenant app, which fights Encore's + autoscaled-Deployment deploy model and Raft's fixed-odd-quorum requirement. +3. If per-replica **local** state is enough instead, you do not need hiqlite at + all: a plain in-process `Map` or the existing Postgres wins, and you dodge + the port-persistence fragility axiomregent had to engineer around. +4. Shape B is a 4-surface fork (core runtime + js bridge + TS SDK + the Go CLI + codegen) of an upstream that ships weekly. Disproportionate. +5. Shape A is the only viable shape, but it is a delivery mechanism for a value + proposition that is not proven for tenant apps today. + +--- + +## Q1 - Encore runtime architecture + +Confirmed napi-rs + tokio, two crates in `github.com/encoredev/encore`: + +| Repo path | Crate | Role | +|---|---|---| +| `runtimes/js/` | `encore-js-runtime` | napi-rs 2.12.2 bridge, `crate-type=["cdylib"]` produces `encore-runtime.node` | +| `runtimes/core/` | `encore-runtime-core` | tokio 1.35.1 + pingora proxy; sqldb/pubsub/cache/objects subsystems | + +- The tokio runtime is **owned and driven by Rust**, not Node's event loop. JS + main thread only `await RT.runForever()`. Request path: pingora/tokio accept + on Rust threads, a napi threadsafe function marshals the handler onto Node's JS + thread, the returned Promise is resolved back into Rust. Encore forked + napi-rs's threadsafe_function to capture handler return values. +- The native module is **not** vendored or on npm; the `encore` CLI fetches + `encore-runtime.node` keyed by `(version, GOOS, GOARCH)` and injects it via + `ENCORE_RUNTIME_LIB`. `encore.dev@1.57.9` ships no `.node`. +- **Extension point for Shape B: none.** Every primitive is a hardcoded + `#[napi]` method on a global `Runtime` singleton; the set + `{sqldb, pubsub, objects, cache, gateway, secrets}` is closed and enumerated + in three must-agree places (core subsystem, napi method, auto-generated + `napi.d.cts`). A new primitive requires forking all of: `encore-runtime-core`, + `encore-js-runtime`, the `encore.dev` TS SDK, **and the Go CLI** (it statically + parses TS to detect primitives; an unrecognized one never lands in + `runtime_config`, so the accessor resolves to nothing). Confirmed high + confidence from code plus Encore docs steering custom infra to "external + resource." +- Encore already ships a **Redis-backed** Cache primitive + (`encore.dev/storage/cache`, RESP via `bb8-redis`). hiqlite is not a Redis + server, so it does not drop in behind that API without speaking RESP or + rewriting `cache/client.rs`. hiqlite maps more naturally onto SQLDatabase than + Cache, an even deeper swap. +- License **MPL-2.0** (file-level copyleft; publish modified Encore files, not + your new ones). Release cadence weekly-to-biweekly (v1.57.1 to v1.57.9 in ~6 + weeks); `runtimes/js/src` target files churn every 1 to 3 months. + +## Q2 - Two tokio runtimes (Shape A) - NOT a blocker + +- hiqlite entry points (`start_node`, `start_node_with_cache`) are `async fn` + that **assume an ambient tokio runtime and never create one**. Full-tree grep: + the only `#[tokio::main]` is in the feature-gated `server` binary, not the + library path. +- Blocking work (WAL writer/reader, SQLite single-writer, KV/dlock handlers) runs + on dedicated `std::thread` OS threads bridged by `flume` channels; it never + occupies a tokio worker or Node's event loop. +- In Shape A the hiqlite addon is a **separate cdylib** (`*.node`) with its own + statically-linked tokio spun up by napi-rs's `tokio_rt` feature. hiqlite runs + on the addon's runtime; Encore runs on Encore's. Two isolated runtimes in one + process is a well-supported pattern (thread pools plus executors, no shared + global to contend). The classic "runtime within a runtime" panic does not apply + because hiqlite nests nothing. +- Idiomatic surface: expose `#[napi] async fn put/get(...)`; napi-rs drives them + on the addon runtime and returns JS promises. Clean. +- **This is sound in theory and trivially confirmed by the PoC.** + +## Q3 - Clustering under horizontal scaling - THE crux + +Reference reality, from the two existing embedders: + +- **rauthy = real N-node Raft cluster**, but the clustering lives entirely in + OAP's Helm chart, not rauthy's code: StatefulSet (`replicas: 3`) + headless + Service (`clusterIP: None`, ports 8100/8200) + `HQL_NODE_ID_FROM=k8s` + (derives node id from pod ordinal) + a **static** `HQL_NODES` list computed at + Helm-render time from replica count and per-ordinal DNS + (`-.:8100`). Scaling replicas means re-render `HQL_NODES` + plus a rolling restart. No gossip / DNS-SRV / dynamic join. +- **axiomregent = single embedded node** (`node_id: 1`, `127.0.0.1`, OS-assigned + ports), explicitly "single-node mode, strictly local, suitable for a desktop + agent." Its complexity budget goes into a `.opc-hiqlite-ports.json` sidecar + that persists the port pair so the committed Raft address stays stable across + restarts; otherwise the in-process client flood-dials a stale unbound port + (os error 61). Even a **single** hiqlite node carries this operational scar. + +hiqlite facts that gate the value: +- With the `cache` feature, the cache is **Raft-replicated across the group** + (not local-per-node). Shared cache implies clustering required. +- Membership is **static** (a `Vec` / `HQL_NODES`), not dynamic discovery. +- Quorum is vanilla Raft majority-of-N: N=1 no fault tolerance, N=2 no benefit + over N=1, N=3 tolerates one loss. Odd counts wanted. + +The tenant-app collision: +- Encore.ts tenant apps deploy as a single-arch amd64 **OCI image** via + `encore build docker` on `ubuntu-latest`, the standard Deployment shape, not + a StatefulSet with stable ordinals and a headless Service. +- A shared-cache embedding would require rebuilding every tenant app as a + StatefulSet with rauthy-style Helm plumbing, and **static membership is + fundamentally incompatible with an HPA autoscaling a Deployment**: every scale + event needs an `HQL_NODES` re-render plus a coordinated rolling restart, and + moving between even/odd replica counts breaks quorum math. +- So the shared-cache case imposes a real distributed-systems tax on every + tenant; the local-state case makes hiqlite unnecessary (in-process map or + Postgres wins). + +**Verdict on Q3: for tenant apps the honest default is per-replica isolation, +which means you probably do not need hiqlite. Shared cross-replica state is +possible but demands StatefulSet Raft clustering that fights Encore's deploy +model, a much bigger commitment than the language boundary the NAPI framing +dissolves.** + +## Q4 - Native binary distribution + +- Deploy target is a **single triple today**: `linux-x64-gnu` (Debian + `node:24-slim`, `ubuntu-latest`, no musl, no multi-arch anywhere). Local dev + adds `darwin-arm64` for `encore run` on Apple Silicon. Minimal 2-triple matrix. +- Distribution precedent already in-tree: the app pulls transitive napi-rs + `.node` binaries (lightningcss, rolldown, fsevents) via npm `os`/`cpu` + optionalDependencies. A hiqlite addon can reuse that exact mechanism. +- **Open unknown (the #1 Shape-A risk):** does `encore build docker` carry a + native runtime `node_module` into the production image, and can the compiled + `main.mjs` `require` it at runtime? The known transitive `.node` deps are + dev/build-time-only for the SPA; they do not prove the API container ships a + runtime-required addon. This is exactly what a PoC de-risks. +- CI cost: cross-building a Rust napi addon for `linux-x64-gnu` from ubuntu is + trivial; `darwin-arm64` needs a macOS runner or cross toolchain. Modest. + +## Q5 - Licensing + +- hiqlite: **Apache-2.0** (confirmed LICENSE plus manifest). Clean. +- Encore: **MPL-2.0** (file-level copyleft). +- OAP posture: **AGPL-3.0**. +- Shape A: an OAP-authored addon linking Apache-2.0 hiqlite is fine. Apache-2.0 + is AGPL-compatible; the addon is AGPL, hiqlite stays Apache. No Encore fork, so + MPL never enters. Shipping prebuilt binaries inside tenant products is fine + under AGPL (offer source for the addon). Clean. +- Shape B: forking MPL-2.0 Encore means modified Encore files stay MPL and must + be published (new files can be AGPL). Manageable legally, but moot given Shape + B is not recommended. + +## Q6 - Upstream-tracking cost + +- Shape B: **high.** Fork tracks a weekly-releasing upstream across 4 surfaces + including the Go CLI codegen; target files change every 1 to 3 months. +- Shape A: **modest.** Track hiqlite (Apache; published 0.13.2, 0.14 + unreleased) and napi-rs. Note the version wrinkle below. + +## Version wrinkle + +The local hiqlite checkout is `0.14.0-pre` (edition 2024, MSRV 1.95, rusqlite +0.40, openraft 0.9.21). Last **published** tag is `0.13.2`. The cache-only +"no data_dir" and `learner_only` features are only in the unreleased pre-version. +OAP already deferred rusqlite 0.40 pending a hiqlite owner bump +(project memory: `project_rusqlite_hiqlite_deferral`). Pin **0.13.2** for any +spike. rauthy uses `0.13` (cache/counters/listen_notify_local/macros); axiomregent +uses `~0.13` (sqlite/dlock/listen_notify_local/auto-heal, cache transitive). + +--- + +## Recommendation + +**Neither shape as a near-term build.** + +- **Reject Shape B outright** on structure: no plugin seam, a 4-surface fork + (including the Go CLI), tracking a weekly upstream, for ergonomics (declared / + auto-provisioned infra) that do not justify the perpetual cost. If Encore-native + DX is ever wanted, the smaller door is contributing/forking the existing Cache + primitive's backend, not a brand-new primitive, and that is still a core-runtime + fork. +- **Hold Shape A in reserve.** It is technically clean (two-tokio safe, 2-triple + distribution, compatible licenses) but it is a mechanism in search of a value + case. Revisit **only** when a concrete requirement appears: tenant apps need a + shared, low-latency cache / dlock / leader-election that Postgres serves poorly + **and** the workload is deployed as a fixed-size StatefulSet (not an autoscaled + Deployment). Absent that, in-process map or Postgres is the right answer and the + `data-redis` to Postgres cleanup should proceed unblocked. Rate-limit stays + Postgres regardless. + +## If you want to de-risk cheaply anyway (optional PoC) + +The one genuinely uncertain, cheap-to-settle thing is the Shape-A **mechanism**, +not the value case. A minimal PoC would prove, in roughly half a day: + +1. A napi-rs addon (`default-features=false, features=["cache","macros"]`, + single-node `Node{id:1,127.0.0.1}`) exposing `#[napi] async fn put/get`. +2. Called from **one** Encore.ts endpoint under a normal `encore run` + (confirms Q2 two-tokio coexistence empirically). +3. Then `encore build docker` and run the image (confirms Q4: does the addon + `.node` survive into the container and load at runtime, the real unknown). + +Scope stays single-node/local; it answers "can this even be wired," not "should +we." It does not touch the rate-limit path and does not block the redis cleanup. diff --git a/docs/analysis/hiqlite-napi-feasibility.md b/docs/analysis/hiqlite-napi-feasibility.md new file mode 100644 index 0000000..82a4528 --- /dev/null +++ b/docs/analysis/hiqlite-napi-feasibility.md @@ -0,0 +1,193 @@ +# Feasibility: hiqlite-in-Encore.ts via NAPI (native, in-process) + +**Verdict: do not build now. Shape B (native Encore primitive) is structurally +disproportionate; Shape A (standalone napi-rs addon) is technically clean and was +proven buildable end to end, but it solves a value problem that does not yet +exist for tenant apps. Recommendation: neither as a near-term build, with Shape A +held in reserve behind a concrete shared-state requirement.** + +Date: 2026-07-05. Spike is strategic/forward-looking; no live gap. + +> **PoC executed.** A minimal Shape A PoC was built and run: a napi-rs addon +> linking `hiqlite` (single-node, cache-only) exposing `put`/`get`, called from +> one Encore.ts endpoint, verified under `encore run` AND inside an +> `encore build docker` image. It works. See +> `experiments/hiqlite-native-spike/` and its `RESULTS.md`. The mechanism working +> does not change the verdict: it proves Shape A is buildable, not that it is +> needed. + +--- + +## TL;DR reasoning chain + +1. The two-tokio-runtime worry (Q2) is **not** the blocker, and the PoC confirms + it. hiqlite never creates its own runtime and pushes blocking I/O onto OS + threads; a napi addon coexists with Encore's Rust runtime as an isolated + second tokio in a separate dylib. +2. The real wall is **Q3, clustering under horizontal scale**. hiqlite's value + in-process (shared cache / dlock / counters / listen_notify) exists **only** + if replicas form one Raft group. That forces StatefulSet + static membership + + headless-Service DNS into every tenant app, which fights Encore's + autoscaled-Deployment deploy model and Raft's fixed-odd-quorum requirement. +3. If per-replica **local** state is enough instead, you do not need hiqlite at + all: a plain in-process `Map` or the existing Postgres wins, and you dodge + the port-persistence fragility axiomregent had to engineer around. +4. Shape B is a 4-surface fork (core runtime + js bridge + TS SDK + the Go CLI + codegen) of an upstream that ships weekly. Disproportionate. +5. Shape A is the only viable shape, and the PoC shows it is buildable, but it is + a delivery mechanism for a value proposition that is not proven for tenant + apps today. + +--- + +## Q1 - Encore runtime architecture + +Confirmed napi-rs + tokio, two crates in `github.com/encoredev/encore`: + +| Repo path | Crate | Role | +|---|---|---| +| `runtimes/js/` | `encore-js-runtime` | napi-rs 2.12.2 bridge, `crate-type=["cdylib"]` produces `encore-runtime.node` | +| `runtimes/core/` | `encore-runtime-core` | tokio 1.35.1 + pingora proxy; sqldb/pubsub/cache/objects subsystems | + +- The tokio runtime is **owned and driven by Rust**, not Node's event loop. JS + main thread only `await RT.runForever()`. Request path: pingora/tokio accept + on Rust threads, a napi threadsafe function marshals the handler onto Node's JS + thread, the returned Promise is resolved back into Rust. +- The native module is **not** vendored or on npm; the `encore` CLI fetches + `encore-runtime.node` keyed by `(version, GOOS, GOARCH)` and injects it via + `ENCORE_RUNTIME_LIB`. Confirmed in the PoC image: + `ENCORE_RUNTIME_LIB=/encore/runtimes/js/encore-runtime.node`, and the build log + shows `Downloading linux/arm64 encore-runtime.node`. +- **Extension point for Shape B: none.** Every primitive is a hardcoded + `#[napi]` method on a global `Runtime` singleton; the set + `{sqldb, pubsub, objects, cache, gateway, secrets}` is closed. A new primitive + requires forking all of: `encore-runtime-core`, `encore-js-runtime`, the + `encore.dev` TS SDK, **and the Go CLI** (it statically parses TS to detect + primitives; an unrecognized one never lands in `runtime_config`). +- Encore already ships a **Redis-backed** Cache primitive + (`encore.dev/storage/cache`, RESP via `bb8-redis`). hiqlite is not a Redis + server, so it does not drop in behind that API without speaking RESP or + rewriting `cache/client.rs`. +- License **MPL-2.0** (file-level copyleft). Release cadence weekly-to-biweekly. + +## Q2 - Two tokio runtimes (Shape A) - CONFIRMED not a blocker + +- hiqlite entry points (`start_node`, `start_node_with_cache`) are `async fn` + that **assume an ambient tokio runtime and never create one**. Blocking work + (WAL writer/reader, SQLite single-writer, KV/dlock handlers) runs on dedicated + `std::thread` OS threads bridged by `flume` channels; it never occupies a tokio + worker or Node's event loop. +- In Shape A the hiqlite addon is a **separate cdylib** (`*.node`) with its own + tokio spun up by napi-rs's `tokio_rt` feature. hiqlite runs on the addon's + runtime; Encore runs on Encore's. Two isolated runtimes in one process. +- **PoC result:** under `encore run`, `put`/`get` round-tripped through the + Encore HTTP layer into napi into hiqlite with no contention, deadlock, or + nested-runtime panic. Confirmed empirically. + +## Q3 - Clustering under horizontal scaling - THE crux (unchanged by the PoC) + +Reference reality, from the two existing embedders: + +- **rauthy = real N-node Raft cluster**, but the clustering lives entirely in + OAP's Helm chart, not rauthy's code: StatefulSet (`replicas: 3`) + headless + Service (`clusterIP: None`, ports 8100/8200) + `HQL_NODE_ID_FROM=k8s` + a + **static** `HQL_NODES` list computed at Helm-render time. No dynamic discovery. +- **axiomregent = single embedded node** (`node_id: 1`, `127.0.0.1`), explicitly + "single-node mode, strictly local, suitable for a desktop agent," and it still + needs a `.opc-hiqlite-ports.json` sidecar to keep the committed Raft address + stable across restarts (else os-error-61 flood). + +hiqlite facts that gate the value: +- With the `cache` feature, the cache is **Raft-replicated across the group** + (not local-per-node). Shared cache implies clustering required. +- Membership is **static** (a `Vec` / `HQL_NODES`), not dynamic discovery. +- Quorum is vanilla Raft majority-of-N: N=1 no fault tolerance, N=2 no benefit + over N=1, N=3 tolerates one loss. Odd counts wanted. + +The tenant-app collision: +- Encore.ts tenant apps deploy as a single-arch amd64 **OCI image / Deployment**, + not a StatefulSet with stable ordinals and a headless Service. +- A shared-cache embedding would require rebuilding every tenant app as a + StatefulSet with rauthy-style Helm plumbing, and **static membership is + fundamentally incompatible with an HPA autoscaling a Deployment**: every scale + event needs an `HQL_NODES` re-render plus a coordinated rolling restart, and + moving between even/odd replica counts breaks quorum math. + +**Verdict on Q3: for tenant apps the honest default is per-replica isolation, +which means you probably do not need hiqlite. Shared cross-replica state is +possible but demands StatefulSet Raft clustering that fights Encore's deploy +model, a much bigger commitment than the language boundary the NAPI framing +dissolves.** + +## Q4 - Native binary distribution - CONFIRMED works, with real costs + +- Deploy target is a **single triple today**: `linux-x64-gnu` (Debian + `node:24-slim`, `ubuntu-latest`, no musl / no multi-arch). Local dev adds + `darwin-arm64`. Minimal 2-triple matrix. +- **PoC result:** `encore build docker` DID carry the addon `.node` into the + image and it loaded at runtime (`put`/`get` worked in-container). But the real + costs surfaced exactly where predicted: + 1. **You cross-build the addon per target yourself.** `encore build docker` + bundles Rust output, it does not compile it. The host darwin `.node` will + not load in a Linux container; a `linux-*-gnu` binary is a separate build + step (a `rust:1-bookworm` container in the PoC; `aws-lc-sys` needs `cmake`). + 2. **Ship a real npm package with per-platform `optionalDependencies`**, not an + npm `file:` symlink. In the PoC the symlink resolved only because + `bundle_source: true` copied its target into the image; that is not a + production pattern. The `lightningcss`/`rolldown` prebuilds already in this + repo are the mechanism to copy. + 3. **`bundle_source` over an npm-workspace root bundles the whole repo.** The + PoC image was 3.73 GB because Encore treated the workspace root as the + bundle root and slurped root `node_modules`, `apps/web*`, and the addon's + Rust `target/` dirs. A `.dockerignore` and a scoped bundle are mandatory. + 4. **Self-host gateway binds `:8080`**, not the infra `base_url` port. +- CI cost: cross-building for `linux-x64-gnu` from ubuntu is trivial; + `darwin-arm64` needs a macOS runner. Modest. + +## Q5 - Licensing + +- hiqlite: **Apache-2.0**. Encore: **MPL-2.0** (file-level copyleft). OAP posture: + **AGPL-3.0**. +- Shape A: an OAP-authored addon linking Apache-2.0 hiqlite is fine (Apache is + AGPL-compatible; no Encore fork, so MPL never enters). Clean. +- Shape B: forking MPL-2.0 Encore means modified Encore files stay MPL and must be + published. Manageable, but moot given Shape B is not recommended. + +## Q6 - Upstream-tracking cost + +- Shape B: **high.** Fork tracks a weekly-releasing upstream across 4 surfaces + including the Go CLI codegen. +- Shape A: **modest.** Track hiqlite (Apache; published 0.13.2, 0.14 unreleased) + and napi-rs. + +## Version wrinkle + +The local hiqlite checkout is `0.14.0-pre` (edition 2024, MSRV 1.95, rusqlite +0.40). Last **published** tag is `0.13.2` (edition 2024, MSRV 1.88). The PoC +pinned **`=0.13.2`** and built cleanly. OAP already deferred rusqlite 0.40 pending +a hiqlite owner bump (OAP memory: `project_rusqlite_hiqlite_deferral`). + +--- + +## Recommendation + +**Neither shape as a near-term build.** + +- **Reject Shape B outright** on structure: no plugin seam, a 4-surface fork + (including the Go CLI), tracking a weekly upstream, for ergonomics (declared / + auto-provisioned infra) that do not justify the perpetual cost. +- **Hold Shape A in reserve.** The PoC proves it is technically clean and + buildable (two-tokio safe, 2-triple distribution, compatible licenses), but it + is a mechanism in search of a value case. Revisit **only** when a concrete + requirement appears: tenant apps need a shared, low-latency cache / dlock / + leader-election that Postgres serves poorly **and** the workload is deployed as + a fixed-size StatefulSet (not an autoscaled Deployment). Absent that, in-process + map or Postgres is the right answer and the `data-redis` to Postgres cleanup + should proceed unblocked. Rate-limit stays Postgres regardless. + +## PoC artifacts + +`experiments/hiqlite-native-spike/` (throwaway, safe to delete): +- `addon/` : the napi-rs crate (`hiqlite-native`), single-node cache-only hiqlite. +- `poc-app/` : minimal standalone Encore.ts app with one service calling the addon. +- `RESULTS.md` : the full empirical run, commands, and caveats. diff --git a/experiments/hiqlite-native-spike/README.md b/experiments/hiqlite-native-spike/README.md new file mode 100644 index 0000000..d122eef --- /dev/null +++ b/experiments/hiqlite-native-spike/README.md @@ -0,0 +1,27 @@ +# Spike: hiqlite-in-Encore.ts via NAPI (native, in-process) + +**Status: throwaway feasibility spike. Not production. Safe to delete.** + +Proves the "Shape A" mechanism from `docs/analysis/hiqlite-napi-feasibility.md`: +a napi-rs addon that links the `hiqlite` Rust crate (single-node, cache-only) +and exposes `put`/`get` to TypeScript, called from one Encore.ts endpoint. + +Two things this spike settles empirically: + +1. **Q2 (two tokio runtimes).** hiqlite's tokio (inside this addon's own + `.node`) coexists with Encore's Rust runtime in one Node process under a + normal `encore run`. +2. **Q4 (native distribution).** Whether `encore build docker` carries a native + `.node` addon into the production image and loads it at runtime. + +## Layout + +- `addon/` : the napi-rs Rust crate (`hiqlite-native`), builds to a `.node`. +- `poc-app/` : a minimal standalone Encore.ts app with one endpoint that calls + the addon. Deliberately separate from `apps/api` so the mechanism question is + not entangled with that app's auth/secrets/Postgres boot path. + +## Findings + +See `RESULTS.md` (written as the spike runs) and the full report at +`docs/analysis/hiqlite-napi-feasibility.md`. diff --git a/experiments/hiqlite-native-spike/RESULTS.md b/experiments/hiqlite-native-spike/RESULTS.md new file mode 100644 index 0000000..8de2a47 --- /dev/null +++ b/experiments/hiqlite-native-spike/RESULTS.md @@ -0,0 +1,119 @@ +# Spike results: hiqlite-in-Encore.ts via NAPI (Shape A) + +Run: 2026-07-05, host = Apple Silicon (aarch64-apple-darwin), Docker daemon up, +`encore v1.57.9`, `rustc 1.95.0` (host) / `1.96.1` (linux builder container), +`@napi-rs/cli 3.1.5`, `hiqlite =0.13.2` (`default-features=false, +features=["cache","macros"]`, so no SQLite/rusqlite C dependency). + +**Bottom line: the Shape A mechanism works end to end. A napi-rs addon that links +hiqlite runs in-process under both `encore run` and inside an `encore build +docker` image, and `put`/`get` round-trips. The real costs are all in native +distribution, not in the runtime coexistence.** + +## What was proven + +### 1. Addon builds and hiqlite runs inside a plain Node process +`napi build --platform --release` linked the full hiqlite tree (openraft, rustls, +reqwest, axum, aws-lc-sys, cryptr, hiqlite-wal) into a 9.9 MB +`hiqlite-native.darwin-arm64.node` in 2m06s. `node sanity.mjs`: hiqlite elected +itself and started in ~2.5s, `put`/`get` round-tripped, absent key returned +`null`. + +### 2. Q2 - two tokio runtimes coexist (CONFIRMED) +Under a normal `encore run`, `GET /hiq/health`, `POST /hiq/put`, +`GET /hiq/get/:key` all worked: +``` +GET /hiq/health -> {"status":"hiqlite-native: ok (single-node, cache-only, in-process)"} +POST /hiq/put spike=... -> {"ok":true} +GET /hiq/get/spike -> {"value":"through encore -> napi -> hiqlite","key":"spike"} +GET /hiq/get/absent -> {"value":null,"key":"absent"} +``` +hiqlite's tokio (inside the addon's own `.node`) and Encore's Rust runtime (inside +`encore-runtime.node`) ran in the same Node process with no contention, deadlock, +or nested-runtime panic. The two runtimes are isolated in separate dynamic +libraries; hiqlite never creates a runtime, it uses the one napi-rs's `tokio_rt` +feature provides for the addon. + +### 3. Q4 - `encore build docker` bundles and loads the native addon (CONFIRMED) +Cross-built the Linux binary natively in a `rust:1-bookworm` container (Debian +glibc, matches `node:24-slim`): `hiqlite-native.linux-arm64-gnu.node` (10.1 MB, +ELF aarch64). `aws-lc-sys` compiled fine on arm64 linux once `cmake` was +installed. + +`encore build docker --config infra.config.json --base node:24-slim --arch arm64 +hiqlite-poc:latest` produced a runnable image. Inside the running container: +``` +GET /hiq/health -> 200 {"status":"hiqlite-native: ok (single-node, cache-only, in-process)"} +POST /hiq/put in-docker=... -> {"ok":true} +GET /hiq/get/in-docker -> {"value":"hiqlite native, bundled + loaded in the encore image","key":"in-docker"} +GET /hiq/get/nope -> {"value":null,"key":"nope"} +``` +The Linux `.node` was carried into the image, loaded at runtime, and hiqlite +started in-process inside the container. + +## Caveats and real costs (these are the findings that matter) + +1. **You must cross-build the addon per deploy target yourself.** `encore build + docker` does NOT compile Rust; it bundles whatever `.node` is present at build + time. The host `.node` is darwin-arm64 and will not load in a Linux container. + Producing the `linux-*-gnu` binary is a separate build step (here: a + `rust:1-bookworm` container). This is the #1 real cost of Shape A and matches + the report's Q4 "native distribution" analysis. In CI the target is + `linux-x64-gnu` (amd64); the same recipe applies with the triple swapped. + +2. **The `file:` dependency worked only by accident of `bundle_source`.** + `node_modules/hiqlite-native` is an npm `file:` symlink to `../addon`. It + resolved in the image ONLY because `bundle_source: true` copied the whole + source tree (including `addon/`) so the symlink target existed inside the + image. A production setup should ship the addon as a real npm package with + per-platform `optionalDependencies` (the same mechanism `lightningcss` / + `rolldown` already use in this repo), not a `file:` symlink. + +3. **`bundle_source` over an npm-workspace root bundles far too much.** Because + `template-encore`'s root `package.json` declares workspaces, `encore build + docker` treated the workspace root as the bundle root and slurped the entire + repo: root `node_modules`, `apps/web*`, AND the addon's Rust `target/` + + `target-linux/` dirs (1.4 GB of build artifacts). Result: a 3.73 GB image / + 886 MB content. A real integration needs a `.dockerignore` (or to keep Rust + build output out of the bundled tree) and a scoped bundle. Not a blocker, but + a required hygiene step. + +4. **The gateway binds `0.0.0.0:8080` in the self-host image**, not the + `base_url` port from the infra config. Map `-p :8080`. (Cost me one + confused "connection reset" before I read the container logs.) + +5. **Cold-start timing.** The first request after boot can reset while hiqlite is + still electing/starting (~2.5s). A real integration should start hiqlite at + service init (or gate readiness) rather than lazily on first request. + +## What this does NOT change + +The mechanism working does not move the strategic verdict. Per +`docs/analysis/hiqlite-napi-feasibility.md`, the value proposition for tenant apps +still hinges on Q3 (shared cross-replica cache requires StatefulSet Raft +clustering that fights Encore's autoscaled-Deployment model). This spike proves +Shape A is *buildable*, not that it is *needed*. It stays in reserve behind a +concrete shared-state requirement. + +## Reproduce + +```bash +cd experiments/hiqlite-native-spike/addon +napi build --platform --release # host .node +node sanity.mjs # plain-Node round-trip + +cd ../poc-app && npm install +encore run # then: curl localhost:4000/hiq/health + +# Linux .node (arm64 shown; use x86_64-unknown-linux-gnu triple for amd64/CI): +cd ../addon +docker run --rm -v "$PWD":/work -v "$HOME/.cargo/registry":/usr/local/cargo/registry \ + -e CARGO_TARGET_DIR=/work/target-linux -w /work rust:1-bookworm \ + bash -c "apt-get update -qq && apt-get install -y -qq cmake && cargo build --release" +cp target-linux/release/libhiqlite_native.so hiqlite-native.linux-arm64-gnu.node + +cd ../poc-app +encore build docker --config infra.config.json --base node:24-slim --arch arm64 hiqlite-poc:latest +docker run -d --name hiqlite-poc-run -p 4000:8080 hiqlite-poc:latest +curl localhost:4000/hiq/health +``` diff --git a/experiments/hiqlite-native-spike/addon/.gitignore b/experiments/hiqlite-native-spike/addon/.gitignore new file mode 100644 index 0000000..7ff8ccf --- /dev/null +++ b/experiments/hiqlite-native-spike/addon/.gitignore @@ -0,0 +1,8 @@ +/target +/target-linux +/node_modules +*.tgz +Cargo.lock +*.node +index.js +index.d.ts diff --git a/experiments/hiqlite-native-spike/addon/Cargo.toml b/experiments/hiqlite-native-spike/addon/Cargo.toml new file mode 100644 index 0000000..657ea8f --- /dev/null +++ b/experiments/hiqlite-native-spike/addon/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "hiqlite-native" +version = "0.0.0" +edition = "2021" +publish = false +description = "Feasibility spike: napi-rs addon linking hiqlite (single-node cache). Throwaway." + +[lib] +crate-type = ["cdylib"] + +[dependencies] +napi = { version = "3", default-features = false, features = ["napi8", "tokio_rt"] } +napi-derive = "3" +# Pinned to the last PUBLISHED hiqlite (the 0.14 line is unreleased; see the report). +# cache-only: no `sqlite` feature, so no bundled rusqlite/SQLite-C -> far more portable. +hiqlite = { version = "=0.13.2", default-features = false, features = ["cache", "macros"] } +tokio = { version = "1", features = ["sync"] } +serde = { version = "1", features = ["derive"] } + +[build-dependencies] +napi-build = "2" + +[profile.release] +lto = true +strip = true diff --git a/experiments/hiqlite-native-spike/addon/build.rs b/experiments/hiqlite-native-spike/addon/build.rs new file mode 100644 index 0000000..0f1b010 --- /dev/null +++ b/experiments/hiqlite-native-spike/addon/build.rs @@ -0,0 +1,3 @@ +fn main() { + napi_build::setup(); +} diff --git a/experiments/hiqlite-native-spike/addon/package.json b/experiments/hiqlite-native-spike/addon/package.json new file mode 100644 index 0000000..01586a2 --- /dev/null +++ b/experiments/hiqlite-native-spike/addon/package.json @@ -0,0 +1,22 @@ +{ + "name": "hiqlite-native", + "version": "0.0.0", + "private": true, + "type": "commonjs", + "main": "index.js", + "types": "index.d.ts", + "napi": { + "binaryName": "hiqlite-native", + "targets": [ + "aarch64-apple-darwin", + "x86_64-unknown-linux-gnu" + ] + }, + "scripts": { + "build": "napi build --platform --release", + "build:debug": "napi build --platform" + }, + "devDependencies": { + "@napi-rs/cli": "3.1.5" + } +} diff --git a/experiments/hiqlite-native-spike/addon/sanity.mjs b/experiments/hiqlite-native-spike/addon/sanity.mjs new file mode 100644 index 0000000..4e1f0fe --- /dev/null +++ b/experiments/hiqlite-native-spike/addon/sanity.mjs @@ -0,0 +1,20 @@ +// Plain-Node sanity check: load the addon, start hiqlite in-process, round-trip. +import { health, put, get } from "./index.js"; + +const t0 = Date.now(); +console.log("health:", await health()); +console.log(` (hiqlite started in ${Date.now() - t0}ms)`); + +await put("greeting", "hello from hiqlite via napi"); +const v = await get("greeting"); +console.log("get greeting:", JSON.stringify(v)); + +const miss = await get("does-not-exist"); +console.log("get missing:", JSON.stringify(miss)); + +if (v !== "hello from hiqlite via napi" || miss !== null) { + console.error("FAIL: round-trip mismatch"); + process.exit(1); +} +console.log("PASS: put/get round-trip through embedded hiqlite"); +process.exit(0); diff --git a/experiments/hiqlite-native-spike/addon/src/lib.rs b/experiments/hiqlite-native-spike/addon/src/lib.rs new file mode 100644 index 0000000..bc8a6bd --- /dev/null +++ b/experiments/hiqlite-native-spike/addon/src/lib.rs @@ -0,0 +1,82 @@ +//! Feasibility spike: a napi-rs addon that links the `hiqlite` Rust crate and +//! exposes a single-node cache (`put`/`get`) to TypeScript. +//! +//! The whole point is that hiqlite runs IN-PROCESS on this addon's own tokio +//! runtime (provided by napi-rs's `tokio_rt` feature). When this `.node` is +//! loaded by a Node process that ALSO hosts Encore's Rust runtime, the two tokio +//! runtimes are isolated in separate dynamic libraries and coexist. This addon +//! proves that empirically. It is NOT production code. + +use hiqlite::macros::CacheVariants; +use hiqlite::{Client, Node, NodeConfig}; +use napi_derive::napi; +use tokio::sync::OnceCell; + +/// hiqlite keys every cache value by a variant of a `CacheVariants` enum. One +/// logical cache is enough for the spike. +#[derive(Debug, CacheVariants)] +enum Cache { + Poc, +} + +/// The embedded hiqlite client, started once on first use and kept for the +/// lifetime of the process. Its Raft/API servers run as background tokio tasks +/// on the addon's runtime. +static CLIENT: OnceCell = OnceCell::const_new(); + +/// Lazily start a single-node, cache-only hiqlite instance bound to loopback. +async fn client() -> napi::Result<&'static Client> { + CLIENT + .get_or_try_init(|| async { + // Isolated, wiped-on-start data dir. cache-only still persists its + // Raft WAL/snapshots to disk by default (cache_storage_disk = true). + let dir = std::env::temp_dir().join("hiqlite-napi-spike"); + let _ = std::fs::remove_dir_all(&dir); + + let config = NodeConfig { + node_id: 1, + nodes: vec![Node { + id: 1, + addr_raft: "127.0.0.1:8100".to_string(), + addr_api: "127.0.0.1:8200".to_string(), + }], + data_dir: dir.to_string_lossy().into_owned().into(), + // Both secrets must be >= 16 chars (hiqlite validates this). + secret_raft: "napi-spike-raft-secret".to_string(), + secret_api: "napi-spike-api-secret0".to_string(), + log_statements: false, + ..NodeConfig::default() + }; + + hiqlite::start_node_with_cache::(config).await + }) + .await + .map_err(|e| napi::Error::from_reason(format!("hiqlite init failed: {e}"))) +} + +/// Confirms the addon loaded and hiqlite started inside this process. +#[napi] +pub async fn health() -> napi::Result { + client().await?; + Ok("hiqlite-native: ok (single-node, cache-only, in-process)".to_string()) +} + +/// Store a string value under `key` (no TTL). +#[napi] +pub async fn put(key: String, value: String) -> napi::Result<()> { + let client = client().await?; + client + .put(Cache::Poc, key, &value, None) + .await + .map_err(|e| napi::Error::from_reason(format!("hiqlite put failed: {e}"))) +} + +/// Read the string value stored under `key`, or `null` if absent. +#[napi] +pub async fn get(key: String) -> napi::Result> { + let client = client().await?; + client + .get::(Cache::Poc, key) + .await + .map_err(|e| napi::Error::from_reason(format!("hiqlite get failed: {e}"))) +} diff --git a/experiments/hiqlite-native-spike/poc-app/.gitignore b/experiments/hiqlite-native-spike/poc-app/.gitignore new file mode 100644 index 0000000..ee19958 --- /dev/null +++ b/experiments/hiqlite-native-spike/poc-app/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +encore.gen/ +.encore/ diff --git a/experiments/hiqlite-native-spike/poc-app/encore.app b/experiments/hiqlite-native-spike/poc-app/encore.app new file mode 100644 index 0000000..496d4b3 --- /dev/null +++ b/experiments/hiqlite-native-spike/poc-app/encore.app @@ -0,0 +1,9 @@ +{ + "id": "", + "lang": "typescript", + "build": { + "docker": { + "bundle_source": true + } + } +} diff --git a/experiments/hiqlite-native-spike/poc-app/hiq/api.ts b/experiments/hiqlite-native-spike/poc-app/hiq/api.ts new file mode 100644 index 0000000..b74791e --- /dev/null +++ b/experiments/hiqlite-native-spike/poc-app/hiq/api.ts @@ -0,0 +1,39 @@ +import { api } from "encore.dev/api"; +// The native addon (CJS napi-rs module). Default-import then destructure is the +// safest ESM<-CJS interop for napi's generated index.js. +import hiqlite from "hiqlite-native"; + +// GET /hiq/health : confirms the addon loaded and hiqlite started in-process, +// i.e. hiqlite's tokio runtime is running alongside Encore's Rust runtime. +export const health = api( + { expose: true, method: "GET", path: "/hiq/health" }, + async (): Promise<{ status: string }> => ({ status: await hiqlite.health() }), +); + +interface PutParams { + key: string; + value: string; +} + +// POST /hiq/put : write a value into the embedded hiqlite cache. +export const put = api( + { expose: true, method: "POST", path: "/hiq/put" }, + async ({ key, value }: PutParams): Promise<{ ok: true }> => { + await hiqlite.put(key, value); + return { ok: true }; + }, +); + +interface GetResponse { + key: string; + value: string | null; +} + +// GET /hiq/get/:key : read it back through the same in-process client. +export const get = api( + { expose: true, method: "GET", path: "/hiq/get/:key" }, + async ({ key }: { key: string }): Promise => ({ + key, + value: await hiqlite.get(key), + }), +); diff --git a/experiments/hiqlite-native-spike/poc-app/hiq/encore.service.ts b/experiments/hiqlite-native-spike/poc-app/hiq/encore.service.ts new file mode 100644 index 0000000..d73a114 --- /dev/null +++ b/experiments/hiqlite-native-spike/poc-app/hiq/encore.service.ts @@ -0,0 +1,5 @@ +import { Service } from "encore.dev/service"; + +// One service, one endpoint set. No middleware, no auth: the spike only needs a +// reachable handler that calls into the native hiqlite addon. +export default new Service("hiq"); diff --git a/experiments/hiqlite-native-spike/poc-app/infra.config.json b/experiments/hiqlite-native-spike/poc-app/infra.config.json new file mode 100644 index 0000000..c32563b --- /dev/null +++ b/experiments/hiqlite-native-spike/poc-app/infra.config.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://encore.dev/schemas/infra.schema.json", + "metadata": { + "app_id": "hiqlite-poc-app", + "env_name": "spike", + "env_type": "production", + "cloud": "local", + "base_url": "http://localhost:4000" + } +} diff --git a/experiments/hiqlite-native-spike/poc-app/package-lock.json b/experiments/hiqlite-native-spike/poc-app/package-lock.json new file mode 100644 index 0000000..e64baa9 --- /dev/null +++ b/experiments/hiqlite-native-spike/poc-app/package-lock.json @@ -0,0 +1,36 @@ +{ + "name": "hiqlite-poc-app", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hiqlite-poc-app", + "version": "0.0.0", + "dependencies": { + "encore.dev": "^1.57.9", + "hiqlite-native": "file:../addon" + } + }, + "../addon": { + "name": "hiqlite-native", + "version": "0.0.0", + "devDependencies": { + "@napi-rs/cli": "3.1.5" + } + }, + "node_modules/encore.dev": { + "version": "1.57.9", + "resolved": "https://registry.npmjs.org/encore.dev/-/encore.dev-1.57.9.tgz", + "integrity": "sha512-xJkB4NRAj4DWl2UK+dnixAGkz+2ZMAMrIkjuQ98aAIaXmA2e33EQtji389n+F3omSvdY9qCLmX1Xvq/kjoOEYQ==", + "license": "MPL-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/hiqlite-native": { + "resolved": "../addon", + "link": true + } + } +} diff --git a/experiments/hiqlite-native-spike/poc-app/package.json b/experiments/hiqlite-native-spike/poc-app/package.json new file mode 100644 index 0000000..15fc72c --- /dev/null +++ b/experiments/hiqlite-native-spike/poc-app/package.json @@ -0,0 +1,10 @@ +{ + "name": "hiqlite-poc-app", + "private": true, + "version": "0.0.0", + "type": "module", + "dependencies": { + "encore.dev": "^1.57.9", + "hiqlite-native": "file:../addon" + } +} diff --git a/experiments/hiqlite-native-spike/poc-app/tsconfig.json b/experiments/hiqlite-native-spike/poc-app/tsconfig.json new file mode 100644 index 0000000..0922f11 --- /dev/null +++ b/experiments/hiqlite-native-spike/poc-app/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "paths": { + "~encore/*": ["./encore.gen/*"] + } + }, + "exclude": ["node_modules", "encore.gen"] +} diff --git a/specs/016-docs-website/spec.md b/specs/016-docs-website/spec.md index 7d235a4..1e07418 100644 --- a/specs/016-docs-website/spec.md +++ b/specs/016-docs-website/spec.md @@ -128,3 +128,12 @@ exits 0 and lists both established paths. not `docs/`. - **Adding publishers to the org allow-list**: governed by spec 012's out-of-scope note; the org controls the allow-list. + +## Amendments + +> **Amended (2026-07-05), self: Redis reference excise.** Removed the +> stale `REDIS_URL` row from `website/docs/reference/configuration.md`: +> Redis is not part of this stack, rate limiting is Postgres-native +> (INV-6, spec 002-security-data-invariants). Content correction only; +> no change to the site's territory, the publish workflow, or any +> acceptance criterion. diff --git a/website/docs/reference/configuration.md b/website/docs/reference/configuration.md index fc224f1..aa6e366 100644 --- a/website/docs/reference/configuration.md +++ b/website/docs/reference/configuration.md @@ -13,7 +13,6 @@ These are typically set in `apps/api/.env` for local development or injected by | `LOG_PII` | Whether to log PII. Must be `false` in production. | `false` | | `PRIVATE_API_BASE_URL` | Upstream URL for the BFF gateway proxy. | - | | `GATEWAY_OAUTH_TOKEN_URL` | Token endpoint for the S2S gateway token. | - | -| `REDIS_URL` | Redis connection string for rate limiting. | - | | `RAUTHY_ISSUER` | OIDC Issuer URL (if `AUTH_DRIVER=rauthy`). | - | | `RAUTHY_REDIRECT_URI` | OIDC Callback URL. | - | | `RAUTHY_SCOPES` | OIDC scopes to request. | `openid profile email groups` |