Uh oh!
There was an error while loading. Please reload this page.
feat(infra): Phase 01 packets 4-6 + ADRs 0029-0031 backend swap - #1
Conversation
Stands up the self-hosted Keycloak identity provider in dev compose with the
two-realm topology that ADR-0004 Amendment 1 + ADR-0019 mandate: `learnstack`
for tenant users and `learnstack-hub` for LearnStack operators. The realms
are hard-isolated — neither realm trusts tokens issued by the other — which is
the load-bearing invariant the Phase-02b OIDC integration and the Hub internal
API surface will both lean on.
Adds
- Keycloak 26 service in `infra/compose/dev.yml` (port 8080, management
port 9000 for /health/ready, depends on healthy Postgres).
- `infra/compose/postgres-init/01-create-keycloak-db.sql` — idempotent
CREATE DATABASE bound to Postgres via docker-entrypoint-initdb.d on first
boot of the `postgres-data` volume.
- `infra/keycloak/realms/learnstack.json` — tenant realm, clients
`learnstack-api` (confidential, service-account + standard + direct grants)
and `learnstack-web` (public PKCE), roles tenant-{admin,instructor,learner},
two demo users wired to Mailpit SMTP.
- `infra/keycloak/realms/learnstack-hub.json` — operator realm, client
`learnstack-hub-web` (public PKCE), roles hub-{platform-admin,operator,
billing-viewer}, one demo operator, CONFIGURE_TOTP required action so the
MFA enrolment flow surfaces in dev (per ADR-0004 Amendment 1: MFA mandatory
in production).
- `infra/keycloak/README.md` — realm matrix, access URLs, cross-trust
invariant, re-seed procedure, what does/does NOT live here.
Docs
- `infra/compose/README.md` regrouped data-plane → identity (packet 4) →
remaining pending packets.
- `docs/roadmap/phase-01-repository-tooling.md` packet-4 status flipped to ✅.
Verification
- Both realm JSONs parse cleanly.
- `docker compose -f infra/compose/dev.yml config -q` exits 0.
- Markdown link sweep on changed docs clean.
- Compose-up smoke test not run in this commit (Keycloak first boot is ~60s);
the packet 5/6 stack will exercise the full stack together.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Stands up the self-hosted live-classroom media plane in dev compose, per ADR-0005 (self-hosted LiveKit OSS is the default supported path). The .NET app never imports the LiveKit server SDK directly — Phase 08c wires the `ILiveClassProvider` abstraction; this packet only ships the runtime LiveKit can hand back tokens against. Adds - `infra/compose/dev.yml` services `livekit` (port 7880 WS signaling, 7881 TCP fallback, 7882 TURN/TLS, 50000-50100/udp media plane) and `coturn` (3478 STUN/TURN, 5349 TURN/TLS, 49152-49200/udp relay range narrowed for dev). - `infra/livekit/livekit.yaml` with the single dev key/secret pair the Phase 08c token issuer will sign with (`devkey` / `devsecret-32-byte-min-length-padding-xyz` — padded to satisfy LiveKit's ≥ 32-byte hard requirement), TURN enabled against the Coturn ports, auto-create + 300s empty-timeout room policy. - `infra/coturn/turnserver.conf` with static long-term credentials (`devuser` / `devsecret`) — fine for a developer poking at TURN with `turnutils_uclient`, production switches to use-auth-secret ephemeral credentials. - `infra/livekit/README.md` documenting the access endpoints, dev credentials, the Phase 08c integration plan, recording + consent + cost-tracking story, and the "what does NOT live here" boundary. Docs - `infra/compose/README.md` gains a "Live media (Phase 01 packet 5)" section; the "does NOT bring up yet" list shrinks by two entries. - `docs/roadmap/phase-01-repository-tooling.md` packet-5 status flipped to ✅. Verification - `docker compose -f infra/compose/dev.yml config -q` exits 0. - YAML parsers accept dev.yml + livekit.yaml. - Markdown link sweep on changed docs clean. - Compose-up smoke test deferred to the packet 6 bundle (LiveKit + Coturn alone don't exercise the eventing/secrets/gateway dependencies yet). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings up every cross-cutting runtime ADR-0014 and ADR-0015 commit the platform to, in one packet so the Phase-02a sockets (`IEventBus`, `ICacheService`, `ISecretProvider`, `IHostToTenantResolver`) can be wired against a real backend the moment Phase 02a starts. Adds (compose services) - `kafka` (KRaft mode, no ZooKeeper) — `confluentinc/cp-kafka:7.8.0`, single-node broker+controller, stable cluster id so the log dir survives restarts, healthchecked via `kafka-topics --list`. - `kafka-ui` (`provectuslabs/kafka-ui:v0.7.2`) on `localhost:8081`. - `vault` (`hashicorp/vault:1.18`) in `-dev` mode with root token `learnstack-dev-root-token`, `IPC_LOCK` cap. - `dapr-placement` + `dapr-sidecar-api` (`daprio/...:1.14.4`) — sidecar app id `learnstack-api`, targets `host.docker.internal:5080` since the .NET host runs OUTSIDE the compose network during active dev. - `apisix` (`apache/apisix:3.10.0-debian`) standalone YAML-reload mode, ports 9080 (HTTP), 9180 (admin), 9443 (HTTPS), 9091 (metrics); `extra_hosts: host.docker.internal:host-gateway` for Linux developers. - `apisix-dashboard` (`apache/apisix-dashboard:3.0.1-alpine`). - `kafka-data` named volume. Adds (configs) - `infra/dapr/components/pubsub-kafka.yaml` — Dapr pub/sub → Kafka. - `infra/dapr/components/statestore-redis.yaml` — Dapr state → Redis. - `infra/dapr/components/secretstore-vault.yaml` — Dapr secrets → Vault. - `infra/dapr/config/dapr-config.yaml` — sampling rate 1, tracing endpoint empty (Phase 11 wires Tempo). - `infra/dapr/README.md` — sidecar topology, application access pattern, what does/does NOT live here. - `infra/apisix/config.yaml` — standalone mode declaration, plugin universe (cors / openid-connect / limit-req / request-id / prometheus / mtls reserved for Phase 02c). - `infra/apisix/apisix.yaml` — three live routes (`/healthz`, `/api/v*/**` OPTIONS preflight, `/api/v*/**` authenticated) all upstreaming to `host.docker.internal:5080`; `/api/internal/*` mTLS route stubbed as a comment block for Phase 02c. - `infra/apisix/dashboard.yaml` — dev-only dashboard auth + allowlist. - `infra/apisix/README.md` — plugin chain walkthrough, route table, upstream addressing rationale, dev credentials, what does/does NOT live here. Docs - `infra/compose/README.md` regrouped to data plane → identity → media → eventing+secrets+Dapr+gateway; "does NOT bring up yet" list shrinks to packets 7-8 only. - `docs/roadmap/phase-01-repository-tooling.md` packet-6 status flipped to ✅. Verification - `docker compose -f infra/compose/dev.yml config -q` exits 0. - Every YAML in the diff (compose, 3 Dapr components, Dapr config, 3 APISIX configs, LiveKit config) parses through PyYAML. - Markdown link sweep on changed docs clean. - Full compose-up smoke test not run in this commit (Keycloak + Kafka cold start alone is ~90s; the DX packet's `make dev` orchestrator will surface a polished startup story). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Acts on two consolidated reviews of commits 2680da9 + be8a10e + 4243f3a. Triaged 3 Blockers + 6 Majors + 9 Minors + 4 Suggestions; took action on every verified-correct finding. Cascade resolutions reduced the change surface: switching APISIX to file-driven `data_plane` mode drops the Admin API + dashboard companion, which in turn eliminates the port-9000 collision with MinIO and the dashboard's etcd connect-retry loop. Blocker - APISIX 3.10 rejects `deployment.role: traditional + config_provider: yaml`; the modern shape is `role: data_plane + role_data_plane. config_provider: yaml`. Switched accordingly. Admin API not exposed in this mode → removed port 9180, removed `enable_admin` + `admin_key`, removed the `apisix-dashboard` service entirely (etcd-less standalone + no Admin API means the dashboard has nothing to read), and deleted `infra/apisix/dashboard.yaml`. - Port 9000 collision (MinIO S3 vs apisix-dashboard) resolved as a side-effect of the dashboard removal above. - Keycloak healthcheck used `curl`, which is NOT in the `quay.io/keycloak/keycloak:26.0` image (UBI-9-minimal + openjdk-21-runtime ship neither curl nor wget). Replaced with the upstream-recommended bash `/dev/tcp` probe against `:9000/health/ready`. Major - Dapr sidecar's documented `host.docker.internal:5080` callback was not realised — `daprd`'s default `-app-channel-address` is `127.0.0.1`, which resolves to inside the sidecar container. Added `-app-channel-address host.docker.internal` to the daprd command, plus the `extra_hosts: host.docker.internal:host-gateway` mapping (Linux developers). Inbound subscription deliveries now reach the workstation `dotnet run` process. - `mtls` was listed in the APISIX plugin universe + referenced as a route-level plugin on the `/api/internal/*` placeholder. APISIX does not implement mTLS as a route plugin; it is enforced on the SSL/SNI object via `client.ca` / `client.depth`. Removed `mtls` from `config.yaml`'s plugin list; rewrote the Phase-02c placeholder in `apisix.yaml` as an `ssls:` entry + `ip-restriction` plugin shape. - `learnstack-hub` realm's demo operator now carries `requiredActions: ["CONFIGURE_TOTP"]` so the MFA enrolment flow actually surfaces on first login (registering the required-action at realm level was insufficient when the user record didn't request it). - `statestore-redis.yaml` declared `actorStateStore: "true"` despite ADR-0014 keeping actors out of scope. Flipped to `"false"` with an inline comment + a callout in the Dapr README. Minor + cleanup - Dapr README ASCII diagram updated (`-resources-path /components`, the current flag name) and gained an explicit Vault-token-duplication callout naming both files that hard-code `learnstack-dev-root-token`. - APISIX README rewritten to reflect data_plane mode + dashboard removal + the SSL-object pattern for mTLS; the "Linux developers must add extra_hosts manually" note is gone (it's wired via the YAML anchor). - LiveKit `--node-ip 127.0.0.1` flag + `rtc.use_external_ip: false` in livekit.yaml now has an inline comment noting the intentional belt-and-braces. - LiveKit healthcheck switched to `wget --spider -q` so a change in the WebSocket-upgrade response doesn't break the probe. - APISIX route 1 (`/healthz`) now restricts to `methods: [GET]` to match the README. - APISIX healthcheck tolerates either 200 or 404 (route absent ⇒ nginx is still listening, which is what the probe should confirm). - Postgres init-script comment now points at the compose README (no imaginary postgres-init/README). - `extra_hosts: host.docker.internal:host-gateway` extracted as a `*host-gateway` YAML anchor and applied to both `apisix` and `dapr-sidecar-api` via `<<: *host-gateway` so future services adding the same need become a one-liner. - `pubsub-kafka.yaml` gained a consumer-group-pinning warning so a future Hub-overlay sidecar doesn't silently split topic partitions by copy-pasting this component. - Compose README's eventing+gateway table re-grouped (admin port + dev credentials lines removed; Kafka in-cluster-only listener note added; Dapr `-app-channel-address` mechanism documented). - Phase 01 roadmap packet-6 status block reflects the data_plane mode + the SSL-object mTLS stub. Verification - `docker compose -f infra/compose/dev.yml config -q` exits 0. - YAML anchor expansion confirmed via `docker compose ... config`: `extra_hosts` materialises on both apisix and dapr-sidecar-api; `-app-channel-address host.docker.internal` appears in the daprd argument list. - 0 YAML / 0 JSON parse failures across all infra files. - Port table: 9000 is now ONLY bound by MinIO; no other service collision. - Markdown link sweep on every changed doc clean. - Live `docker compose up` smoke test still pending (Docker daemon unreachable on this workstation); ride along with the Phase 07 (DX) `make dev` smoke. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fork Picks up upstream patches and minors across every pinned image where a straightforward upgrade is safe. MinIO is deliberately left alone here — the Bitnami repo was archived and the project is migrating to SeaweedFS in a separate ADR-gated commit. Redis 7.4 and Postgres 16 are intentionally kept at the current major because a bump there is a license / migration decision that needs its own ADR. Upgrades - Postgres 16.6 → 16.14-alpine (16.x patch) - Mailpit v1.21 → v1.29.7 - Meilisearch v1.11 → v1.44.0 (33 minors; dev compose, `down -v` re-seeds) - Keycloak 26.0 → 26.6.2 (26.x patch + minor) - LiveKit v1.8.0 → v1.12.0 - Coturn 4.6 → 4.11.0 - Confluent CP Kafka 7.8.0 → 8.2.1 (CP 8.x ⇒ Kafka 4.x; KRaft stays the default broker mode so the existing env-var block continues to apply) - Vault 1.18 → 1.21.4 (community edition; Enterprise's 2.0 jump does not affect this image) - Dapr daprd + placement 1.14.4 → 1.17.7 - APISIX 3.10.0-debian → 3.16.0-debian (data_plane mode unchanged) Fork swap - kafka-ui: `provectuslabs/kafka-ui:v0.7.2` → `ghcr.io/kafbat/kafka-ui:latest` The upstream `provectuslabs/kafka-ui` had no release since 2024-04 and is effectively abandoned with known CVEs; `kafbat/kafka-ui` is the active community fork sharing the same env-var contract. Deliberately NOT bumped (need their own ADR) - Redis 7.4 → 8.x: the 8.x line is tri-licensed (AGPL+SSPL+RSALv2). Choice between Redis 8.x and the Valkey fork is a license + governance call. - Postgres 16 → 17/18: major upgrades affect extension compatibility + RLS-specific defaults; needs a migration plan. Already-removed (out of scope for this packet, not a regression) - APISIX dashboard was removed in 07175ba when APISIX switched to file-driven standalone mode (no etcd ⇒ no dashboard). Compose README port table updated to match. Verification - `docker compose -f infra/compose/dev.yml config -q` exits 0. - No port mappings changed; no name collisions. - Live `docker compose up` smoke test still pending (Docker daemon unreachable on this workstation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands the architectural decision that the dev compose + the `IStorageProvider` adapter will follow when switching object-storage backends. The chain - ADR-0029 is the new decision (Accepted, 2026-05-19): self-hosted SeaweedFS behind the existing S3-shaped `IStorageProvider` contract, Apache 2.0 license, no phone-home, single-binary self-hostable, S3 gateway compatibility sufficient for everything `IStorageProvider` consumes today. - ADR-0002 gets Amendment 1 narrowed to the storage row only — every other choice in ADR-0002 (.NET 10, EF Core, Postgres, Redis, Next.js, modular monolith) is unchanged. The amendment cites the trigger (MinIO `minio/minio` repo archived 2026-04 + the licensing trajectory that removed the no-phone-home posture ADR-0020 Self-Hosted Air-Gapped depends on) and points forward to ADR-0029. - `decisions/README.md` active-ADR table gets a row for 0029 in its numeric slot. Hard-rule check - ADR numbers stay sequential (0029 is the next free slot; 0023-0028 are reserved drafts per the existing reservation table). ADR-0029 takes the next unused number, not a reserved one. - ADR-0002's Decision section is not rewritten; the Amendment-block pattern this repo already uses for ADR-0003/0004/0006/0010 is applied verbatim. - No fifth Hub endpoint, no Verticals folder, no domain-flavoured names; ADR-0014's three Dapr building blocks unchanged. The compose service swap + the 27-doc reference sweep land in a separate commit so the ADR can be reviewed in isolation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands the ADR-0029 decision: the dev compose `minio` service becomes `seaweedfs` (single binary packing master + volume + filer + S3 gateway) and every MinIO reference in the corpus moves to SeaweedFS — except the two intentional historical mentions in ADR-0002 (Amendment 1) and ADR-0029 (the decision record), plus the inline "replaces / matches the prior MinIO endpoint" notes in dev.yml + compose README + seaweedfs/README that document the migration. The S3 access surface is a drop-in: SeaweedFS S3 gateway is port-mapped to the same host ports MinIO used (9000 = S3 API, 9001 = filer UI in place of the old MinIO console), and the dev S3 credentials match the prior defaults (`learnstack` / `learnstack-dev-secret`) so any local script, env var, or test fixture continues to work unchanged. Adds - `infra/compose/dev.yml`: `seaweedfs` service (`chrislusf/seaweedfs:3.94`, command-mode `server -dir=/data -master -volume -filer -s3 -s3.port=8333 -s3.config=/etc/s3-identities.json -metricsPort=9091`), healthcheck against `/cluster/healthz` on the master HTTP API. - `infra/seaweedfs/s3-identities.json` — S3 identity config matching the prior MinIO root credentials (dev-only; production loads from Vault). - `infra/seaweedfs/README.md` — access surface, dev credentials, tenant key-prefix isolation rule (unchanged), re-seed procedure, what does NOT live here. Removes - `infra/compose/dev.yml`: `minio` service definition + `MINIO_*` env vars + the `minio-data` named volume (replaced by `seaweedfs-data`). - Per-file `MinIO`/`minio` references across 26 docs (Standards 00 / 06 / 09 / 10 / 11 / 12 / 15 / 20; Architecture 02 / 03 / 04 / 05 / 07 / 08 / 09 / 16 / 18 / 23 / 25 / 29 / 32; Decisions 0003 / 0014 / 0017 / 0018 / README), plus glossary, root README, CLAUDE.md, 4 roadmap phase docs, `.gitignore` (`minio-data/` → `seaweedfs-data/`), and 4 skill files. Preserved (intentional) - `docs/decisions/0002-initial-architecture.md` — Amendment 1 names the swap explicitly. - `docs/decisions/0029-object-storage-seaweedfs.md` — the decision record. - `infra/compose/dev.yml` inline comments — "SeaweedFS replaces MinIO", "matches the prior MinIO endpoint", "matches prior MinIO credentials", "Filer UI (replaces the MinIO console)" — historical context for the next reader. - `infra/compose/README.md` — "applied to MinIO, it applies to SeaweedFS" — the tenant-key-prefix rule is backend-independent. - `infra/seaweedfs/README.md` — references the prior MinIO surface for drop-in continuity. Verification - `docker compose -f infra/compose/dev.yml config -q` exits 0. - Compose-config expansion shows `seaweedfs` service + `seaweedfs-data` volume; `minio` references are gone outside the four intentional sites. - All markdown link sweeps on changed docs clean. - Live `docker compose up` smoke test deferred (Docker daemon not running on this workstation); will ride along with Phase 07's `make dev` smoke. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-up architectural decisions landed together because they share the trigger — do major-version + vendor calls while LearnStack is still pre-implementation, so the migration drag is zero. The matching dev compose swap + the doc sweep ride along in the same commit per the "single coherent change" approach the user asked for. ADRs added - ADR-0030: Redis-compatible store moves to Valkey (Linux Foundation, BSD-3-Clause). Drop-in on RESP protocol — Dapr `state.redis` component, `StackExchange.Redis` library, `ICacheService` over Dapr all continue to work; those names are protocol/library/Dapr-provider- type identifiers, not vendor brands. Trigger: Redis Inc.'s 2024-03 license shift removed the unambiguous BSD path; the triple-license (AGPLv3 / RSALv2 / SSPLv1) leaves SaaS + Self-Hosted ambiguity that Valkey resolves without code change. - ADR-0031: PostgreSQL major version pinned to 18.x across all deployment modes. Longest LTS runway (EOL 2030-11), native `gen_uuid_v7()` that the ADR-0023 draft can adopt without an extension, async I/O for sequential scans that benefits the partitioned `audit_log` operator queries. RLS policy syntax + connection-string + role provisioning unchanged from 16 / 17, so the tenant-isolation defense-in-depth pattern (ADR-0003) transfers verbatim. ADR-0002 — Amendment 2 (dated 2026-05-19) bundles both decisions without rewriting ADR-0002's Decision section. decisions/README — two new rows in the active table; the prior "ADR-0029 partially supersedes ADR-0002's SeaweedFS row" wording was a sed-sweep artefact from the earlier MinIO→SeaweedFS commit and is corrected here to "MinIO row" while we're in the index. Compose changes - `postgres:16.14-alpine` → `postgres:18.4-alpine`. - `redis` service → `valkey` service; image `redis:7.4-alpine` → `valkey/valkey:8.1-alpine`; healthcheck `redis-cli ping` → `valkey-cli ping`; volume `redis-data` → `valkey-data`; the `dapr-sidecar-api` `depends_on` entry switches to `valkey`. - Dapr statestore component (`infra/dapr/components/statestore-redis.yaml`): `redisHost: redis:6379` → `redisHost: valkey:6379`. File name keeps the `-redis` suffix because `state.redis` is the Dapr provider-type identifier (RESP-protocol adapter), not the vendor name. Long inline comment explains the distinction. - `.gitignore`: `redis-data/` → `valkey-data/`. Doc sweep — selective - Vendor / image / boring-choice mentions of "Redis" → "Valkey" (Standards 00 § 9, CLAUDE.md hard rules, README.md, glossary, roadmap, compose README, deployment-mode tables, cost model, architecture diagrams, observability standards). - "PostgreSQL 16" / "Postgres 16" → "PostgreSQL 18" / "Postgres 18" across docs, standards, and roadmap. - Library / protocol / Dapr-provider-type names PRESERVED: `StackExchange.Redis`, `IConnectionMultiplexer`, `state.redis` Dapr component, `Microsoft.Extensions.Caching.Redis`, `RedisCacheService` example name. These are not the vendor brand; they are the wire protocol or the .NET type. Restored explicitly after the bulk replace to avoid the rename trap. Preserved intentional context - ADR-0002 Amendment 1 + 2 reference MinIO / Redis as the prior choices being superseded. - ADR-0029 + ADR-0030 + ADR-0031 — the decision records. - ADR-0014 (Adopt Dapr) — `StackExchange.Redis` library reference + Phase 2 restore. - `infra/compose/dev.yml` inline comment — "Valkey replaces Redis per ADR-0030". - `infra/dapr/components/statestore-redis.yaml` — file name + type identifier + inline comment explaining the protocol-vs-vendor split. - `architecture/29-dapr-integration.md` — `StackExchange.Redis` + `RedisCacheService` example name preserved. Deliberately NOT done - `infra/dapr/components/statestore-redis.yaml` file rename. The `state.redis` Dapr provider-type convention means the suffix mirrors the wire protocol; renaming would obscure that signal. - A separate "deprecate Redis 7.4 in production" migration plan — pre-implementation means no production Redis exists yet, no data migration is needed; Phase 11 production deployment lands directly on Valkey. Verification - `docker compose -f infra/compose/dev.yml config -q` exits 0. - Compose-config expansion shows `valkey` service + `valkey-data` volume + `postgres:18.4-alpine`. - Dapr statestore component `redisHost: valkey:6379` verified. - `grep -ln 'PostgreSQL 16\|Postgres 16'` returns zero outside ADR-0002 + ADR-0031. - `grep -ln '\bRedis\b'` matches only the 6 intentional historical context sites (ADR-0002, ADR-0030, ADR-0014, ADR-0029, dev.yml comment, statestore-redis.yaml comment, dapr-integration library-reference text). - Markdown link sweep on every changed doc clean. - Live `docker compose up` smoke deferred (Docker daemon not running on this workstation); will ride along with Phase 07's `make dev`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Sorry @cemililik, your pull request is larger than the review limit of 150000 diff characters
📝 WalkthroughWalkthroughThis PR modernizes LearnStack's infrastructure stack by adopting Valkey as the Redis-compatible cache/state backend, SeaweedFS as the object storage backend, and PostgreSQL 18 as the primary RDBMS, formalized through three new architecture decision records (ADRs 0029–0031) and accompanied by comprehensive updates to architecture documentation, development standards, roadmap, and a substantial expansion of the local Docker Compose development environment with Keycloak identity, LiveKit media, Kafka events, Vault secrets, Dapr sidecar, and APISIX API gateway. ChangesInfrastructure Modernization: Valkey + SeaweedFS + PostgreSQL 18
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Code Review
This pull request executes a significant infrastructure migration by replacing MinIO with SeaweedFS for object storage and Redis with Valkey for caching and state management, while also pinning PostgreSQL to major version 18. It completes the local development stack by integrating Keycloak, LiveKit, Kafka, Vault, Dapr, and APISIX into the Docker Compose environment. The changes include extensive updates to architectural documentation, ADRs, and test configurations to align with these new backend selections. I have no feedback to provide as there were no review comments.
There was a problem hiding this comment.
Actionable comments posted: 8
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
docs/standards/05-database.md (1)
4-10:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd ADR-0031 to the standards provenance.
Line 16 introduces a non-trivial baseline change (
PostgreSQL 18+), but the Derives from list does not cite the new decision record. Please addADR-0031in the header so the rule is traceable.Suggested patch
**Derives from:** [ADR-0002 Initial Architecture](../decisions/0002-initial-architecture.md), [ADR-0003 Tenant Isolation Defense in Depth](../decisions/0003-tenant-isolation-defense-in-depth.md) (Amendment 1: Organization Scope), [ADR-0006 Events and Outbox](../decisions/0006-events-and-outbox.md) (Amendment 1: Dapr pub/sub dispatch transport), [ADR-0014 Adopt Dapr](../decisions/0014-adopt-dapr.md), -[ADR-0017 Tenant + Organization Hierarchy](../decisions/0017-tenant-organization-hierarchy.md).+[ADR-0017 Tenant + Organization Hierarchy](../decisions/0017-tenant-organization-hierarchy.md),+[ADR-0031 PostgreSQL Major Version](../decisions/0031-postgresql-major-version.md).As per coding guidelines, "Standards changes must cite an ADR for non-trivial rule changes or new rules".
Also applies to: 16-16
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/standards/05-database.md` around lines 4 - 10, The standards header "Derives from:" is missing a reference to ADR-0031 despite a non-trivial baseline change noting "PostgreSQL 18+"; update the "Derives from:" list to include ADR-0031 so the provenance is complete—locate the header block that begins with "**Derives from:**" and add "ADR-0031" (with its title if you follow the existing pattern) to the comma-separated entries.docs/standards/06-testing.md (1)
10-19:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd diagram title and text fallback for the test pyramid.
The Mermaid block is missing the required title + bullet fallback, so it is not fully readable in plain-text contexts.
As per coding guidelines, "Use Mermaid for diagrams in fenced
mermaidblocks; diagrams must remain readable in text form with titles and bullet fallbacks".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/standards/06-testing.md` around lines 10 - 19, The Mermaid diagram lacks a human-readable title and plain-text fallback; update the fenced ```mermaid``` block that defines nodes e2e, contract, integration, arch, and unit by adding a visible diagram title (e.g., "Test Pyramid" via a title statement) and include a short bullet-list fallback immediately before or after the mermaid block that lists the same five items (End-to-end / Playwright, Contract & API tests, Integration tests, Architecture tests, Unit tests) so the diagram remains understandable in plain-text contexts.docs/standards/12-infrastructure.md (1)
52-70:⚠️ Potential issue | 🟠 Major | ⚡ Quick winLocal compose stack list is stale (
redis,apisix-dashboard).In this block, Line 54 still lists
redisand Line 69 listsapisix-dashboard, but this PR’s infra direction is Valkey + APISIX data_plane (dashboard removed). Please update this list to avoid operator/developer confusion.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/standards/12-infrastructure.md` around lines 52 - 70, Update the local compose stack list by removing the stale entries "redis" and "apisix-dashboard" and replacing them with the new infra pieces: add "valkey" and "apisix-data_plane" (or "apisix-data-plane" to match naming convention used elsewhere); edit the block that currently contains "redis" and "apisix-dashboard" so the list reflects Valkey + APISIX data_plane instead of the old Redis and dashboard entries..claude/skills/local-dev-setup/SKILL.md (2)
48-49:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse the current deployment mode names (
SelfHostedOnline/SelfHostedAirGapped).This skill still documents
SelfHosted, which conflicts with the current split-mode model used elsewhere. Please update both the inputs and mode table to the two explicit values.Also applies to: 171-174
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.claude/skills/local-dev-setup/SKILL.md around lines 48 - 49, Update the deployment mode documentation to use the current split-mode names: replace any occurrence of "SelfHosted" in the Deployment mode table and input descriptions with the two explicit values "SelfHostedOnline" and "SelfHostedAirGapped" (e.g., update the table row that currently lists `Development / SaaS / Dedicated / SelfHosted` to `Development / SaaS / Dedicated / SelfHostedOnline / SelfHostedAirGapped`), and make the same replacements in the inputs section referenced around the later occurrence (the block near the original 171-174). Ensure both the table header/values and any example input keys or explanatory text (the lines mentioning deployment mode) consistently use the new names.
115-116:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove APISIX dashboard guidance if data-plane stack no longer ships it.
The line still advertises an optional dashboard, but this PR’s infra fixes explicitly removed dashboard usage for APISIX data_plane mode. Keeping this here will send devs to a non-existent surface.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.claude/skills/local-dev-setup/SKILL.md around lines 115 - 116, Remove the obsolete "APISIX dashboard (optional) | 9000 | Route inspection." table row from the SKILL.md table: locate the row that advertises the APISIX dashboard and delete it (or replace it with a note that the APISIX data-plane no longer includes a dashboard) so the ports/hosts list no longer points developers to a non-existent dashboard surface.docs/roadmap/phase-01-repository-tooling.md (1)
175-175:⚠️ Potential issue | 🟠 Major | ⚡ Quick winResolve APISIX dashboard contradiction in this phase doc.
Line 175 says
apisix-dashboard (dev only), but Lines 46-47 state standalonedata_planewith no companion dashboard. Keep one authoritative statement here to prevent insecure or invalid setup assumptions.Proposed doc fix
-- **APISIX** (standalone YAML-reload mode) + apisix-dashboard (dev only).+- **APISIX** (standalone YAML-reload mode, `data_plane`; no Admin API/dashboard).Based on learnings: Single source of truth: each piece of knowledge lives in exactly one place (glossary for terms, ADRs for decisions, standards for ongoing rules, architecture docs for conceptual descriptions, roadmap for phases).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/roadmap/phase-01-repository-tooling.md` at line 175, The roadmap currently contradicts itself about APISIX: change the phrasing so only one authoritative statement exists by either removing "apisix-dashboard (dev only)" from the APISIX line or updating the earlier "data_plane" mention to explicitly include the dashboard; reference the unique terms APISIX, apisix-dashboard, and data_plane to locate the lines and make them consistent, and add a short note pointing readers to the canonical source (glossary/ADR/architecture doc) for the final decision to enforce single-source-of-truth.CLAUDE.md (1)
29-33:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate Phase-01 status text to match current scope.
The status paragraph still says Keycloak/LiveKit/Kafka/Vault/Dapr/APISIX will “land incrementally,” but this PR already includes those packets. This is now stale and onboarding-misleading.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@CLAUDE.md` around lines 29 - 33, The Phase-01 status paragraph in CLAUDE.md still claims Keycloak/LiveKit/Kafka/Vault/Dapr/APISIX “will land incrementally”; update that paragraph to reflect that those packets are already included in this PR by replacing the future-tense wording with present-tense confirmation (e.g., state that Keycloak, LiveKit, Kafka, Vault, Dapr, and APISIX are included), remove the misleading “will land incrementally” phrase, and adjust any remaining scope/next-steps text to accurately list only items truly outstanding; look for the "Phase-01" status paragraph and the mention of Keycloak/LiveKit/Kafka/Vault/Dapr/APISIX to make this change.
🟡 Minor comments (13)
docs/decisions/0014-adopt-dapr.md-15-19 (1)
15-19:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAlign component file names with current infra paths.
Line 17–19 still reference generic component filenames, but this PR’s infra layer uses
infra/dapr/components/pubsub-kafka.yamlandinfra/dapr/components/statestore-redis.yaml. Please update the table paths to avoid stale guidance.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/decisions/0014-adopt-dapr.md` around lines 15 - 19, Update the component file paths in the DAPR decision table to match the infra layer names: replace `dapr/components/pubsub.yaml` with `infra/dapr/components/pubsub-kafka.yaml` and replace `dapr/components/statestore.yaml` with `infra/dapr/components/statestore-redis.yaml` (leave the secret store `secretstore-vault.yaml` entry as-is if it already matches `infra/dapr/components/secretstore-vault.yaml`); locate the table in docs/decisions/0014-adopt-dapr.md and update the three Component file cell values accordingly.docs/decisions/0002-initial-architecture.md-76-77 (1)
76-77:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winCorrect ADR-0023 link target.
Line 76 links
[ADR-0023 draft]toREADME.mdinstead of the ADR file, which makes navigation ambiguous.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/decisions/0002-initial-architecture.md` around lines 76 - 77, The link target for the text "[ADR-0023 draft]" incorrectly points at README.md; update the link so the anchor points to the actual ADR-0023 markdown file (replace README.md with the ADR-0023 filename) while keeping the link text unchanged, ensuring the reference resolves to the ADR document rather than README.docs/decisions/0031-postgresql-major-version.md-22-23 (1)
22-23:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix broken ADR-0023 references.
Line 22 and Line 181 link ADR-0023 to
README.md, which resolves to the decisions index, not the ADR itself. Please point both references to the actual ADR-0023 file path.Also applies to: 181-182
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/decisions/0031-postgresql-major-version.md` around lines 22 - 23, The markdown links in this document that reference "ADR-0023 (Strongly-typed ID source generator)" currently point to README.md; update both occurrences of that link (the one near the top and the one later in the file) to point to the actual ADR-0023 markdown file (replace README.md with the ADR-0023 file name/slug for the Strongly-typed ID ADR) so the link resolves directly to the ADR-0023 document.docs/architecture/09-tenant-isolation.md-198-201 (1)
198-201:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd a language tag to the fenced block.
The storage example block is missing a fenced code language specifier, which triggers markdown lint (MD040).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/architecture/09-tenant-isolation.md` around lines 198 - 201, The fenced code block showing the storage example (the block containing "tenants/{tenant_id}/organizations/{org_id}/courses/{course_id}/... ← org-scoped" and "tenants/{tenant_id}/brand/... ← tenant-wide") is missing a language tag which triggers MD040; add an appropriate language identifier (e.g., "text" or "yaml") after the opening triple backticks of that fenced block so the markdown linter recognizes the language and the lint error is resolved.docs/architecture/04-technical-architecture.md-10-10 (1)
10-10:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAlign DB version wording with the pinned major policy.
Line 10 says
PostgreSQL 18+, but this PR’s architecture decision is a pin to 18.x. Please tighten this to avoid drift in upgrade expectations.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/architecture/04-technical-architecture.md` at line 10, The DB version wording should be tightened from a floating "PostgreSQL 18+" to the pinned major as described in ADR-0003; update the table entry that currently reads "PostgreSQL 18+ (shared schema + RLS isolation; ADR-0003)" to the explicit pinned-major wording like "PostgreSQL 18.x (shared schema + RLS isolation; ADR-0003)" so the architecture doc and ADR align.docs/architecture/04-technical-architecture.md-208-212 (1)
208-212:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix stale
redisnaming in Local Infrastructure list.This block still lists
rediswhile the document now standardizes on Valkey; update the service label to keep setup docs consistent.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/architecture/04-technical-architecture.md` around lines 208 - 212, Replace the stale "redis" entry in the Local Infrastructure list with the standardized service name "Valkey" (match casing used elsewhere in the docs), i.e., update the list item in the block containing "postgres / redis / seaweedfs / meilisearch" so it reads "postgres / Valkey / seaweedfs / meilisearch"; ensure any other nearby occurrences in that same Local Infrastructure section are also renamed to "Valkey" for consistency.docs/architecture/29-dapr-integration.md-83-83 (1)
83-83:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winClarify Valkey-vs-Redis naming in the state store snippet.
This section declares a Valkey state store, but the sample still uses Redis-shaped host/secret identifiers. Add a one-line note here that
state.redis/redis*keys are Dapr component naming, while runtime points to Valkey, to avoid operator misconfiguration.Suggested doc tweak
### `statestore.yaml` — Valkey state store +Note: Dapr uses the `state.redis` component and `redis*` metadata names for Redis-compatible backends; in LearnStack this backend is Valkey.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/architecture/29-dapr-integration.md` at line 83, Add a one-line clarifying note under the "statestore.yaml — Valkey state store" heading that explains the apparent Redis-shaped keys are Dapr component naming only (e.g., state.redis / redis*), and that although the keys look like Redis identifiers the runtime component is configured to use Valkey; reference "statestore.yaml", "Valkey", and the "state.redis"/"redis*" keys in the note so operators understand the naming vs actual provider..claude/skills/add-feature-key/SKILL.md-268-270 (1)
268-270:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAlign cache TTL guidance with the infrastructure standard.
This currently states the Valkey layer is 60s TTL, but the standard cache policy uses 60s for L1 and 15-min upper bound for L2/Valkey. Please sync wording to prevent conflicting implementation guidance.
Based on learnings, “Single source of truth: each piece of knowledge lives in exactly one place.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.claude/skills/add-feature-key/SKILL.md around lines 268 - 270, Update the TTL guidance in the SKILL.md paragraph referencing the Valkey layer and IsEnabledAsync: change the sentence that currently says "Valkey layer (60s TTL...)" to state the standard cache policy—L1 TTL 60s and L2/Valkey TTL up to 15 minutes (with Valkey still eager-invalidated by the Dapr event), and add a short note reinforcing "Single source of truth: each piece of knowledge lives in exactly one place." Ensure you update the line mentioning the Valkey layer and IsEnabledAsync so readers get the correct L1/L2 TTL values and invalidation behavior.README.md-23-25 (1)
23-25:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix stale Phase-01 packet status in the top-level README.
This still says Keycloak/LiveKit/Kafka/Vault/Dapr/APISIX “land in subsequent Phase-01 packets,” but those packets are already present in this PR.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 23 - 25, Update the README.md sentence that says "land in subsequent Phase-01 packets" to reflect that Keycloak, LiveKit, Kafka, Vault, Dapr, APISIX (and other items listed) are now included in this PR; specifically edit the phrase containing "land in subsequent Phase-01 packets" and, if needed, adjust or remove the reference to docs/roadmap/phase-01-repository-tooling.md so the top-level README accurately states these components are present in the current Phase-01 packet rather than pending.README.md-33-34 (1)
33-34:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAlign Valkey version text with compose pin.
Line 33 documents “Valkey 7,” but
infra/compose/dev.ymlpinsvalkey/valkey:8.1-alpine. Keep the README version in sync to avoid setup confusion.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 33 - 34, The README's dependency list currently says "Valkey 7" but the compose pin in infra/compose/dev.yml uses valkey/valkey:8.1-alpine; update the README text to match the pinned version (e.g., change "Valkey 7" to "Valkey 8.1" or "Valkey 8.1-alpine") so the documentation and the infra compose pin are consistent; ensure the same phrasing appears where the README references Valkey in the Cache / Pub-Sub / Secrets line.infra/dapr/README.md-23-23 (1)
23-23:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd a language to the fenced code block (markdownlint MD040).
The fence starting at Line 23 should include a language (e.g.,
text) to satisfy linting.Suggested fix
-```+```text ┌──────────────────────────┐ ┌──────────────────────────────────────────┐ ... -```+```🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@infra/dapr/README.md` at line 23, Update the fenced code block shown in the diff by adding a language identifier to the opening triple backticks (e.g., change ``` to ```text) so the markdownlint MD040 rule is satisfied; locate the code fence that contains the ASCII box art and add "text" (or another appropriate language) after the opening backticks.infra/compose/dev.yml-277-277 (1)
277-277:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFollow Kafka KRaft's official
CLUSTER_IDgeneration method.The value
learnstack-dev-cluster-id-1does not follow the standard Kafka format. KRaft expectsCLUSTER_IDto be a URL-safe Base64-encoded UUID generated viakafka-storage.sh random-uuid, not a custom string. While this custom value matches the allowed character set and won't cause initialization failure on an already-established cluster, it diverges from Kafka's design and best practices.Generate and use the proper format:
- CLUSTER_ID: learnstack-dev-cluster-id-1+ CLUSTER_ID: <output from: kafka-storage.sh random-uuid>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@infra/compose/dev.yml` at line 277, The CLUSTER_ID value currently set as learnstack-dev-cluster-id-1 does not follow Kafka KRaft's required format; replace the hard-coded CLUSTER_ID in the dev.yml service environment with a KRaft-generated UUID (run kafka-storage.sh random-uuid on your Kafka binaries or CI runner) and paste that URL-safe Base64-encoded UUID into the CLUSTER_ID environment variable so the KRaft broker uses the official cluster identifier format.infra/apisix/README.md-34-38 (1)
34-38:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd a language identifier to the fenced plugin-chain block.
Line 34 starts a fenced block without a language (
MD040), which can trip markdownlint in CI.♻️ Proposed fix
-```+```text real-ip → cors → openid-connect → limit-req → request-id → proxy-rewrite → upstream ↓ prometheus (response)</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.In
@infra/apisix/README.mdaround lines 34 - 38, The fenced code block showing
the plugin chain (the triple-backtick block containing "real-ip → cors →
openid-connect → limit-req → request-id → proxy-rewrite → upstream") is missing
a language identifier and triggers MD040; update that fenced block to include a
language tag such as "text" (e.g., changetotext) so markdownlint stops
flagging it, leaving the block content unchanged.</details> </blockquote></details> </blockquote></details> <details> <summary>🧹 Nitpick comments (2)</summary><blockquote> <details> <summary>infra/compose/dev.yml (1)</summary><blockquote> `293-293`: _⚡ Quick win_ **Pin `kafka-ui` to a fixed image tag.** Using `:latest` makes the dev stack non-reproducible and can introduce surprise breakages between runs. <details> <summary>Suggested fix</summary> ```diff - image: ghcr.io/kafbat/kafka-ui:latest + image: ghcr.io/kafbat/kafka-ui:<pinned-version> ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@infra/compose/dev.yml` at line 293, Replace the non-reproducible image reference "ghcr.io/kafbat/kafka-ui:latest" for the kafka-ui service with a fixed, explicit tag (e.g. ghcr.io/kafbat/kafka-ui:<VERSION>) or an environment-variable-backed tag (e.g. ghcr.io/kafbat/kafka-ui:${KAFKA_UI_TAG}) to pin the image; update the image line in the kafka-ui service block (replace ":latest") and document/set the chosen <VERSION> or KAFKA_UI_TAG in your dev environment or .env so runs are reproducible. ``` </details> </blockquote></details> <details> <summary>infra/dapr/config/dapr-config.yaml (1)</summary><blockquote> `11-14`: _⚡ Quick win_ **Disable tracing sampling when no Zipkin endpoint is configured.** With `samplingRate: "1"` and an empty `zipkin.endpointAddress`, the sidecar generates spans that fail to export, creating unnecessary telemetry overhead in development. Set sampling to `"0"` until an exporter endpoint is available. <details> <summary>Suggested adjustment</summary> ```diff spec: tracing: - samplingRate: "1" - zipkin: - endpointAddress: "" + samplingRate: "0" features: [] ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@infra/dapr/config/dapr-config.yaml` around lines 11 - 14, The dapr config currently sets samplingRate: "1" while zipkin.endpointAddress is empty, causing generated spans to fail exporting; change the samplingRate value to "0" when zipkin.endpointAddress is not configured (i.e., set samplingRate: "0") so tracing is disabled until a valid Zipkin endpoint is provided—update the samplingRate entry in the config (referencing the samplingRate and zipkin.endpointAddress keys) accordingly. ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.Inline comments:
In@backend/Directory.Packages.props:
- Line 40: The PackageVersion entry for Testcontainers.Valkey (PackageVersion
Include="Testcontainers.Valkey" Version="3.10.0") references a package that does
not exist on NuGet and causes restore failures; remove this PackageVersion or
replace it with the correct package name/version (for example remove
Testcontainers.Valkey if unused, or change to the proper Testcontainers.*
package such as Testcontainers.PostgreSql if that was intended) and ensure any
project references relying on Testcontainers.Valkey are updated to the correct
package symbol.In
@docs/architecture/08-livekit-cost-model.md:
- Line 225: The table row string "LiveKit OSS Hetzner | 3× SFU + 2× Egress +
Valkey + TURN + 1 TB recording storage ≈ $650–800" understates recording
storage: replace the "1 TB recording storage" with the correct estimate based on
the document's ~60 MB/min assumption (300,000 minutes → ~18 TB) and update the
cost line accordingly (adjust the ≈ $650–800 to reflect the higher storage
cost or annotate that storage is additional ~18 TB), ensuring the same table
cell/row format and the exact phrase "LiveKit OSS Hetzner" is preserved.In
@docs/decisions/0002-initial-architecture.md:
- Around line 21-22: The ADR-0002 decision section has been edited (lines
replacing the original "decision/consequence" content) which must remain
immutable; revert any changes made to ADR-0002's decision body and restore the
original text, then create a new amendment or superseding ADR that records the
backend stack changes (PostgreSQL 18.x, Valkey, SeaweedFS, .NET 10, etc.) and
reference ADR-0002 from that new ADR; ensure the file named
0002-initial-architecture.md contains only the original accepted decision text
and move all suggested backend updates into a separate ADR or amendment document
that supersedes or amends ADR-0002.In
@docs/standards/20-infrastructure-stack.md:
- Around line 194-195: The sentence that currently groups "in-process / Valkey
layer" as having a 60s TTL must be split: update the wording so that the
in-process (L1) cache TTL is stated as 60s, and the Valkey (L2) cache TTL is
described as having a 15-minute upper bound; keep the note that eager
invalidation flows from the Dapr event and that the TTL is a safety net. Locate
and edit the sentence mentioning "in-process / Valkey layer" and replace it with
two concise clauses referencing L1 (in-process) = 60s and L2/Valkey = 15 min
upper bound while preserving the Dapr invalidation and safety-net wording.- Around line 52-53: Update the standards doc to cite the relevant ADRs for the
infrastructure rule changes: add links/references to ADR-0029 and ADR-0030 next
to the table entries and header where Cache (InMemoryCacheService →
DaprCacheService → Valkey) and Secrets (EnvironmentSecretProvider →
DaprSecretProvider → Vault) are described, and include ADR-0031 as well if
Postgres policy (any Postgres mentions) is affected; ensure the ADR IDs are
clearly referenced in the standards header/reference list and adjacent to the
specific symbols InMemoryCacheService, DaprCacheService, Valkey,
EnvironmentSecretProvider, DaprSecretProvider, Vault, and Postgres mentioned in
the document.In
@infra/compose/dev.yml:
- Around line 93-97: The compose file maps two services to the same host port
"8080:8080" (SeaweedFS volume HTTP API and Keycloak), causing a bind collision;
update one of the mappings (e.g., change Keycloak's host binding from
"8080:8080" to an unused host port like "8081:8080" or change SeaweedFS instead)
so the host ports are unique, and then update any references (docs, env vars,
tests) that expect the old host port; locate the mappings by the port strings
"8080:8080" and the service names SeaweedFS/Keycloak in the compose YAML and
change the left-hand host port accordingly.In
@infra/keycloak/realms/learnstack-hub.json:
- Line 79: Change the public client object where "fullScopeAllowed" is set to
true: set "fullScopeAllowed" to false and add explicit client scopes or
scopeMappings that grant only the necessary roles to the client token.
Specifically, update the client’s "defaultClientScopes" or add
"scopeMappings"/"roleMappings" entries that include the operator portal roles
(e.g., hub-operator and any other required roles such as hub-platform-admin or
hub-billing-viewer as appropriate) so tokens retain needed role claims; ensure
these mappings reference the realm roles used by your operator portal.In
@infra/keycloak/realms/learnstack.json:
- Around line 68-75: The learnstack-api client config is over-permissive:
disable direct access grants by setting "directAccessGrantsEnabled" to false
(remove deprecated direct grant use for a confidential service-account), replace
the wildcard webOrigins entry ("webOrigins": ["+"]) with an empty array or
explicit origins (e.g., []) so backend-only clients have no permissive CORS
origins, and ensure "fullScopeAllowed" is explicitly set to false for the client
(add "fullScopeAllowed": false) to prevent automatic granting of all
roles/scopes to the service account; keep "serviceAccountsEnabled": true and
only whitelist necessary "redirectUris" if required.Outside diff comments:
In @.claude/skills/local-dev-setup/SKILL.md:
- Around line 48-49: Update the deployment mode documentation to use the current
split-mode names: replace any occurrence of "SelfHosted" in the Deployment mode
table and input descriptions with the two explicit values "SelfHostedOnline" and
"SelfHostedAirGapped" (e.g., update the table row that currently listsDevelopment / SaaS / Dedicated / SelfHostedtoDevelopment / SaaS / Dedicated / SelfHostedOnline / SelfHostedAirGapped), and make the same replacements in
the inputs section referenced around the later occurrence (the block near the
original 171-174). Ensure both the table header/values and any example input
keys or explanatory text (the lines mentioning deployment mode) consistently use
the new names.- Around line 115-116: Remove the obsolete "APISIX dashboard (optional) | 9000 |
Route inspection." table row from the SKILL.md table: locate the row that
advertises the APISIX dashboard and delete it (or replace it with a note that
the APISIX data-plane no longer includes a dashboard) so the ports/hosts list no
longer points developers to a non-existent dashboard surface.In
@CLAUDE.md:
- Around line 29-33: The Phase-01 status paragraph in CLAUDE.md still claims
Keycloak/LiveKit/Kafka/Vault/Dapr/APISIX “will land incrementally”; update that
paragraph to reflect that those packets are already included in this PR by
replacing the future-tense wording with present-tense confirmation (e.g., state
that Keycloak, LiveKit, Kafka, Vault, Dapr, and APISIX are included), remove the
misleading “will land incrementally” phrase, and adjust any remaining
scope/next-steps text to accurately list only items truly outstanding; look for
the "Phase-01" status paragraph and the mention of
Keycloak/LiveKit/Kafka/Vault/Dapr/APISIX to make this change.In
@docs/roadmap/phase-01-repository-tooling.md:
- Line 175: The roadmap currently contradicts itself about APISIX: change the
phrasing so only one authoritative statement exists by either removing
"apisix-dashboard (dev only)" from the APISIX line or updating the earlier
"data_plane" mention to explicitly include the dashboard; reference the unique
terms APISIX, apisix-dashboard, and data_plane to locate the lines and make them
consistent, and add a short note pointing readers to the canonical source
(glossary/ADR/architecture doc) for the final decision to enforce
single-source-of-truth.In
@docs/standards/05-database.md:
- Around line 4-10: The standards header "Derives from:" is missing a reference
to ADR-0031 despite a non-trivial baseline change noting "PostgreSQL 18+";
update the "Derives from:" list to include ADR-0031 so the provenance is
complete—locate the header block that begins with "Derives from:" and add
"ADR-0031" (with its title if you follow the existing pattern) to the
comma-separated entries.In
@docs/standards/06-testing.md:
- Around line 10-19: The Mermaid diagram lacks a human-readable title and
plain-text fallback; update the fencedmermaidblock that defines nodes
e2e, contract, integration, arch, and unit by adding a visible diagram title
(e.g., "Test Pyramid" via a title statement) and include a short bullet-list
fallback immediately before or after the mermaid block that lists the same five
items (End-to-end / Playwright, Contract & API tests, Integration tests,
Architecture tests, Unit tests) so the diagram remains understandable in
plain-text contexts.In
@docs/standards/12-infrastructure.md:
- Around line 52-70: Update the local compose stack list by removing the stale
entries "redis" and "apisix-dashboard" and replacing them with the new infra
pieces: add "valkey" and "apisix-data_plane" (or "apisix-data-plane" to match
naming convention used elsewhere); edit the block that currently contains
"redis" and "apisix-dashboard" so the list reflects Valkey + APISIX data_plane
instead of the old Redis and dashboard entries.Minor comments:
In @.claude/skills/add-feature-key/SKILL.md:
- Around line 268-270: Update the TTL guidance in the SKILL.md paragraph
referencing the Valkey layer and IsEnabledAsync: change the sentence that
currently says "Valkey layer (60s TTL...)" to state the standard cache policy—L1
TTL 60s and L2/Valkey TTL up to 15 minutes (with Valkey still eager-invalidated
by the Dapr event), and add a short note reinforcing "Single source of truth:
each piece of knowledge lives in exactly one place." Ensure you update the line
mentioning the Valkey layer and IsEnabledAsync so readers get the correct L1/L2
TTL values and invalidation behavior.In
@docs/architecture/04-technical-architecture.md:
- Line 10: The DB version wording should be tightened from a floating
"PostgreSQL 18+" to the pinned major as described in ADR-0003; update the table
entry that currently reads "PostgreSQL 18+ (shared schema + RLS isolation;
ADR-0003)" to the explicit pinned-major wording like "PostgreSQL 18.x (shared
schema + RLS isolation; ADR-0003)" so the architecture doc and ADR align.- Around line 208-212: Replace the stale "redis" entry in the Local
Infrastructure list with the standardized service name "Valkey" (match casing
used elsewhere in the docs), i.e., update the list item in the block containing
"postgres / redis / seaweedfs / meilisearch" so it reads "postgres / Valkey /
seaweedfs / meilisearch"; ensure any other nearby occurrences in that same Local
Infrastructure section are also renamed to "Valkey" for consistency.In
@docs/architecture/09-tenant-isolation.md:
- Around line 198-201: The fenced code block showing the storage example (the
block containing
"tenants/{tenant_id}/organizations/{org_id}/courses/{course_id}/... ←
org-scoped" and "tenants/{tenant_id}/brand/...
← tenant-wide") is missing a language tag which triggers MD040; add an
appropriate language identifier (e.g., "text" or "yaml") after the opening
triple backticks of that fenced block so the markdown linter recognizes the
language and the lint error is resolved.In
@docs/architecture/29-dapr-integration.md:
- Line 83: Add a one-line clarifying note under the "statestore.yaml — Valkey
state store" heading that explains the apparent Redis-shaped keys are Dapr
component naming only (e.g., state.redis / redis*), and that although the keys
look like Redis identifiers the runtime component is configured to use Valkey;
reference "statestore.yaml", "Valkey", and the "state.redis"/"redis*" keys in
the note so operators understand the naming vs actual provider.In
@docs/decisions/0002-initial-architecture.md:
- Around line 76-77: The link target for the text "[ADR-0023 draft]" incorrectly
points at README.md; update the link so the anchor points to the actual ADR-0023
markdown file (replace README.md with the ADR-0023 filename) while keeping the
link text unchanged, ensuring the reference resolves to the ADR document rather
than README.In
@docs/decisions/0014-adopt-dapr.md:
- Around line 15-19: Update the component file paths in the DAPR decision table
to match the infra layer names: replacedapr/components/pubsub.yamlwithinfra/dapr/components/pubsub-kafka.yamland replacedapr/components/statestore.yamlwithinfra/dapr/components/statestore-redis.yaml(leave the secret storesecretstore-vault.yamlentry as-is if it already matchesinfra/dapr/components/secretstore-vault.yaml); locate the table in
docs/decisions/0014-adopt-dapr.md and update the three Component file cell
values accordingly.In
@docs/decisions/0031-postgresql-major-version.md:
- Around line 22-23: The markdown links in this document that reference
"ADR-0023 (Strongly-typed ID source generator)" currently point to README.md;
update both occurrences of that link (the one near the top and the one later in
the file) to point to the actual ADR-0023 markdown file (replace README.md with
the ADR-0023 file name/slug for the Strongly-typed ID ADR) so the link resolves
directly to the ADR-0023 document.In
@infra/apisix/README.md:
- Around line 34-38: The fenced code block showing the plugin chain (the
triple-backtick block containing "real-ip → cors → openid-connect → limit-req →
request-id → proxy-rewrite → upstream") is missing a language identifier and
triggers MD040; update that fenced block to include a language tag such as
"text" (e.g., changetotext) so markdownlint stops flagging it, leaving
the block content unchanged.In
@infra/compose/dev.yml:
- Line 277: The CLUSTER_ID value currently set as learnstack-dev-cluster-id-1
does not follow Kafka KRaft's required format; replace the hard-coded CLUSTER_ID
in the dev.yml service environment with a KRaft-generated UUID (run
kafka-storage.sh random-uuid on your Kafka binaries or CI runner) and paste that
URL-safe Base64-encoded UUID into the CLUSTER_ID environment variable so the
KRaft broker uses the official cluster identifier format.In
@infra/dapr/README.md:
- Line 23: Update the fenced code block shown in the diff by adding a language
identifier to the opening triple backticks (e.g., changetotext) so the
markdownlint MD040 rule is satisfied; locate the code fence that contains the
ASCII box art and add "text" (or another appropriate language) after the opening
backticks.In
@README.md:
- Around line 23-25: Update the README.md sentence that says "land in subsequent
Phase-01 packets" to reflect that Keycloak, LiveKit, Kafka, Vault, Dapr, APISIX
(and other items listed) are now included in this PR; specifically edit the
phrase containing "land in subsequent Phase-01 packets" and, if needed, adjust
or remove the reference to docs/roadmap/phase-01-repository-tooling.md so the
top-level README accurately states these components are present in the current
Phase-01 packet rather than pending.- Around line 33-34: The README's dependency list currently says "Valkey 7" but
the compose pin in infra/compose/dev.yml uses valkey/valkey:8.1-alpine; update
the README text to match the pinned version (e.g., change "Valkey 7" to "Valkey
8.1" or "Valkey 8.1-alpine") so the documentation and the infra compose pin are
consistent; ensure the same phrasing appears where the README references Valkey
in the Cache / Pub-Sub / Secrets line.Nitpick comments:
In@infra/compose/dev.yml:
- Line 293: Replace the non-reproducible image reference
"ghcr.io/kafbat/kafka-ui:latest" for the kafka-ui service with a fixed, explicit
tag (e.g. ghcr.io/kafbat/kafka-ui:) or an environment-variable-backed
tag (e.g. ghcr.io/kafbat/kafka-ui:${KAFKA_UI_TAG}) to pin the image; update the
image line in the kafka-ui service block (replace ":latest") and document/set
the chosen or KAFKA_UI_TAG in your dev environment or .env so runs are
reproducible.In
@infra/dapr/config/dapr-config.yaml:
- Around line 11-14: The dapr config currently sets samplingRate: "1" while
zipkin.endpointAddress is empty, causing generated spans to fail exporting;
change the samplingRate value to "0" when zipkin.endpointAddress is not
configured (i.e., set samplingRate: "0") so tracing is disabled until a valid
Zipkin endpoint is provided—update the samplingRate entry in the config
(referencing the samplingRate and zipkin.endpointAddress keys) accordingly.</details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `15b5e51a-3551-4fd2-9d46-b1f9ef741786` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between da08550bbe241589a66c93a1510dc3589b013715 and 3fd9054bea79f9e3f276092400f62d0dfee21fec. </details> <details> <summary>📒 Files selected for processing (73)</summary> * `.claude/skills/README.md` * `.claude/skills/add-feature-key/SKILL.md` * `.claude/skills/add-integration-test/SKILL.md` * `.claude/skills/local-dev-setup/SKILL.md` * `.claude/skills/run-tests-locally/SKILL.md` * `.claude/skills/standards-check/SKILL.md` * `.gitignore` * `CLAUDE.md` * `README.md` * `backend/Directory.Packages.props` * `backend/README.md` * `backend/tests/LearnStack.Tests.Integration/LearnStack.Tests.Integration.csproj` * `docs/architecture/02-domain-model.md` * `docs/architecture/03-module-boundaries.md` * `docs/architecture/04-technical-architecture.md` * `docs/architecture/05-mvp-scope.md` * `docs/architecture/06-extension-model.md` * `docs/architecture/07-in-app-live-classroom.md` * `docs/architecture/08-livekit-cost-model.md` * `docs/architecture/09-tenant-isolation.md` * `docs/architecture/16-media-pipeline.md` * `docs/architecture/18-webrtc-build-vs-adopt.md` * `docs/architecture/21-feature-flags.md` * `docs/architecture/23-data-protection.md` * `docs/architecture/24-learnstack-hub.md` * `docs/architecture/25-deployment-models.md` * `docs/architecture/29-dapr-integration.md` * `docs/architecture/32-tenant-customization-model.md` * `docs/decisions/0002-initial-architecture.md` * `docs/decisions/0003-tenant-isolation-defense-in-depth.md` * `docs/decisions/0010-cross-module-communication.md` * `docs/decisions/0014-adopt-dapr.md` * `docs/decisions/0017-tenant-organization-hierarchy.md` * `docs/decisions/0018-tenant-driven-customization-model.md` * `docs/decisions/0022-custom-domain-tls.md` * `docs/decisions/0029-object-storage-seaweedfs.md` * `docs/decisions/0030-redis-compatible-store-valkey.md` * `docs/decisions/0031-postgresql-major-version.md` * `docs/decisions/README.md` * `docs/glossary.md` * `docs/roadmap/phase-00-product-architecture.md` * `docs/roadmap/phase-01-repository-tooling.md` * `docs/roadmap/phase-02a-kernel-tenancy.md` * `docs/roadmap/phase-04-cms-media-pages.md` * `docs/roadmap/phase-08c-classroom.md` * `docs/standards/00-principles.md` * `docs/standards/05-database.md` * `docs/standards/06-testing.md` * `docs/standards/09-error-handling.md` * `docs/standards/10-observability.md` * `docs/standards/11-security.md` * `docs/standards/12-infrastructure.md` * `docs/standards/15-performance.md` * `docs/standards/20-infrastructure-stack.md` * `infra/apisix/README.md` * `infra/apisix/apisix.yaml` * `infra/apisix/config.yaml` * `infra/compose/README.md` * `infra/compose/dev.yml` * `infra/compose/postgres-init/01-create-keycloak-db.sql` * `infra/coturn/turnserver.conf` * `infra/dapr/README.md` * `infra/dapr/components/pubsub-kafka.yaml` * `infra/dapr/components/secretstore-vault.yaml` * `infra/dapr/components/statestore-redis.yaml` * `infra/dapr/config/dapr-config.yaml` * `infra/keycloak/README.md` * `infra/keycloak/realms/learnstack-hub.json` * `infra/keycloak/realms/learnstack.json` * `infra/livekit/README.md` * `infra/livekit/livekit.yaml` * `infra/seaweedfs/README.md` * `infra/seaweedfs/s3-identities.json` </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| "pkce.code.challenge.method": "S256", | ||
| "post.logout.redirect.uris": "http://localhost:3100/*" | ||
| }, | ||
| "fullScopeAllowed": true |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
In Keycloak 26.x, what is the security impact of setting fullScopeAllowed=true on a public OIDC client, and what is the recommended default for least-privilege setups?
💡 Result:
Setting fullScopeAllowed=true on a public OIDC client in Keycloak makes the client issue access tokens containing the user’s full set of roles (including roles beyond what’s explicitly mapped for that client), rather than restricting the token to the roles/scope mappings intended for least-privilege use.[1] This can effectively broaden the authorization surface: if the token contains additional roles, downstream resource servers that rely on token role claims may grant more access than intended. For least-privilege setups, the recommended default is to disable it: set fullScopeAllowed=false (i.e., keep token role delegation restricted to explicitly configured scopes/role mappings). This aligns with Keycloak community/security guidance pushing toward setting the flag to false by default for new clients to avoid over-privileged tokens.[2][3]
Citations:
- 1: https://linuxdatahub.com/keycloak-full-scope-allowed/
- 2: Change the default value of fullScopeAllowed to false keycloak/keycloak#37927
- 3: Proposal to Set Full Scope Allowed to False by Default for New Clients keycloak/keycloak#34334
🏁 Script executed:
# First, locate and verify the file exists
find . -name "learnstack-hub.json" -type fRepository: cemililik/LearnStack
Length of output: 108
🏁 Script executed:
# Read the specific file to see the context around line 79if [ -f"infra/keycloak/realms/learnstack-hub.json" ];then
sed -n '75,85p' infra/keycloak/realms/learnstack-hub.json
elseecho"File not found at expected path"fiRepository: cemililik/LearnStack
Length of output: 364
🏁 Script executed:
# Get the full client configuration to understand scope mappingsif [ -f"infra/keycloak/realms/learnstack-hub.json" ];then# Find the client name and its configuration structure
jq '.clients[] | select(.clientId == "hub-public" or .publicClient == true) | {clientId, publicClient, fullScopeAllowed, redirectUris, scope, protocolMappers}' infra/keycloak/realms/learnstack-hub.json 2>/dev/null | head -50
fiRepository: cemililik/LearnStack
Length of output: 254
🏁 Script executed:
# Check if there are any scope or role mappings defined for clientsif [ -f"infra/keycloak/realms/learnstack-hub.json" ];then# Look for scope and role mappings structure
jq '.scopeMappings // .clientScopeMappings' infra/keycloak/realms/learnstack-hub.json 2>/dev/null | head -100
fiRepository: cemililik/LearnStack
Length of output: 69
🏁 Script executed:
# Get complete client configuration including scope detailsif [ -f"infra/keycloak/realms/learnstack-hub.json" ];then
jq '.clients[] | select(.clientId == "learnstack-hub-web")' infra/keycloak/realms/learnstack-hub.json 2>/dev/null
fiRepository: cemililik/LearnStack
Length of output: 705
🏁 Script executed:
# Check for client scope bindings and defaultsif [ -f"infra/keycloak/realms/learnstack-hub.json" ];then
jq '.clientScopes // empty' infra/keycloak/realms/learnstack-hub.json 2>/dev/null | head -80
fiRepository: cemililik/LearnStack
Length of output: 46
🏁 Script executed:
# Check if there are any role mappings or scope mappings defined at realm level for this clientif [ -f"infra/keycloak/realms/learnstack-hub.json" ];then
jq '.roles, .roleMappings' infra/keycloak/realms/learnstack-hub.json 2>/dev/null | head -50
fiRepository: cemililik/LearnStack
Length of output: 709
🏁 Script executed:
# Check for any nested clientScopeMappings structureif [ -f"infra/keycloak/realms/learnstack-hub.json" ];then
jq '.clientScopeMappings // .scopeMappings[] | select(.client == "learnstack-hub-web") // empty' infra/keycloak/realms/learnstack-hub.json 2>/dev/null
fiRepository: cemililik/LearnStack
Length of output: 46
🏁 Script executed:
# Check full structure to see if defaultClientScopes exists in client configif [ -f"infra/keycloak/realms/learnstack-hub.json" ];then
jq '.clients[] | select(.clientId == "learnstack-hub-web") | keys' infra/keycloak/realms/learnstack-hub.json 2>/dev/null
fiRepository: cemililik/LearnStack
Length of output: 334
🏁 Script executed:
# Verify actual size and structure of the config file to make sure we're seeing the full client configif [ -f"infra/keycloak/realms/learnstack-hub.json" ];then
wc -l infra/keycloak/realms/learnstack-hub.json
# Also check if there are defaultClientScopes or similar in client arrays
jq '.clients[] | select(.clientId == "learnstack-hub-web") | {defaultClientScopes, optionalClientScopes, scopeMappings, clientScopeMappings}' infra/keycloak/realms/learnstack-hub.json 2>/dev/null
fiRepository: cemililik/LearnStack
Length of output: 232
Restrict fullScopeAllowed and configure explicit scope mappings for the Hub public client.
fullScopeAllowed: true on the public client currently grants access to all user roles (hub-platform-admin, hub-operator, hub-billing-viewer) regardless of least-privilege principle. Changing this setting alone to false will break the operator portal unless explicit scope or role mappings are simultaneously configured to include the required roles in the token.
To properly harden this:
- Set
fullScopeAllowedtofalse - Configure
defaultClientScopesor add scope mappings that explicitly include the roles the operator portal requires
Without step 2, the portal will lose access to role claims and authentication will fail.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@infra/keycloak/realms/learnstack-hub.json` at line 79, Change the public
client object where "fullScopeAllowed" is set to true: set "fullScopeAllowed" to
false and add explicit client scopes or scopeMappings that grant only the
necessary roles to the client token. Specifically, update the client’s
"defaultClientScopes" or add "scopeMappings"/"roleMappings" entries that include
the operator portal roles (e.g., hub-operator and any other required roles such
as hub-platform-admin or hub-billing-viewer as appropriate) so tokens retain
needed role claims; ensure these mappings reference the realm roles used by your
operator portal.
| "directAccessGrantsEnabled": true, | ||
| "serviceAccountsEnabled": true, | ||
| "redirectUris": [ | ||
| "http://localhost:5080/*", | ||
| "http://localhost:3000/*" | ||
| ], | ||
| "webOrigins": ["+"], | ||
| "protocol": "openid-connect", |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For Keycloak 26.x confidential API clients, are directAccessGrantsEnabled=true, webOrigins=["+"], and fullScopeAllowed=true recommended? What secure defaults are advised for service-account-oriented backends?
💡 Result:
No—those values are generally not recommended for Keycloak 26.x confidential (service-account) API clients; secure defaults are to keep password-style direct access disabled, avoid broad CORS exposure, and avoid “full scope” so tokens remain least-privilege. 1) directAccessGrantsEnabled=true Not recommended. Keycloak’s “direct access grant” corresponds to the OAuth Resource Owner Password Credentials Grant (ROPCG), which is widely treated as insecure/deprecated (OAuth 2.1 removes it) and is recommended to be disabled by default for new clients. Keycloak maintainers explicitly propose setting directAccessGrantsEnabled/“Direct access grant” to false by default for security reasons. [1][2] 2) webOrigins=["+"] Not a security recommendation for service-account backends. Web Origins controls CORS behavior for browser-based requests; Keycloak docs describe it as a per-client CORS allowlist policy. [3] Using + is convenience for mapping from Valid Redirect URIs, but if you are building a backend using client credentials/service accounts, you typically do not need browser CORS at all—so safest practice is to restrict/omit Web Origins rather than rely on +. (Also, other Keycloak discussions clarify that + has limitations vs wildcard redirect URIs and that * may be required to truly permit all, indicating it can be easy to mis-assume the scope of “allow all”.) [4] 3) fullScopeAllowed=true Not recommended. For service-account-oriented backends, Keycloak’s own service-account documentation warns that the “Full Scope Allowed” switch is useful only for development, and recommends disabling it in production and properly configuring role scope mappings instead. [5] Keycloak maintainers also discuss the security need to change/disable the default because it can lead to over-privileged tokens, with proposals to set fullScopeAllowed to false by default. [6][7][8] Recommended secure defaults for confidential service-account backends (client credentials) - Keep “Client authentication” / confidential access type on (service account usage depends on enabling service account roles and client credentials). [5] - Disable “Direct access grant” (i.e., keep directAccessGrantsEnabled=false). [1][2] - Do not configure permissive Web Origins for token endpoints/backend APIs; leave it empty unless you have a browser flow that truly needs CORS. Web Origins is a client-level CORS allowlist. [3] - Set fullScopeAllowed=false and explicitly map/assign only the required roles via scope/role mappings; Keycloak notes this switch is dev-only and should be disabled in production. [5] If you tell me whether your backend is strictly token-exchange-only (client_credentials to resource server) or also serves browser traffic (Swagger/CORS), I can suggest a concrete minimal client configuration set for Keycloak 26.x.
Citations:
- 1: Admin-UI: disable Direct Access Grant by default when creating a new client keycloak/keycloak#30226
- 2: Set directAccessGrantsEnabled to false keycloak/keycloak#36871
- 3: https://github.com/keycloak/keycloak/blob/main/docs/documentation/server_admin/topics/clients/oidc/con-basic-settings.adoc
- 4: Web Origins don't follow Redirect URI wildcard keycloak/keycloak#44069
- 5: https://github.com/keycloak/keycloak/blob/main/docs/documentation/server_admin/topics/clients/oidc/proc-using-a-service-account.adoc
- 6: Change the default value of fullScopeAllowed to false keycloak/keycloak#37927
- 7: Proposal to Set Full Scope Allowed to False by Default for New Clients keycloak/keycloak#34334
- 8: https://www.keycloak.org/docs-api/26.2.5/javadocs/org/keycloak/services/clientpolicy/executor/FullScopeDisabledExecutor.html
🏁 Script executed:
#!/bin/bash# Check if file exists and verify the content at specified lines
file_path="infra/keycloak/realms/learnstack.json"if [ -f"$file_path" ];thenecho"=== File exists ==="echo"Total lines: $(wc -l <"$file_path")"echo""echo"=== Content around lines 68-75 and line 80 ==="
sed -n '60,85p'"$file_path"| cat -n | sed "s/^[[:space:]]*//"| awk '{print NR+59": "$0}'elseecho"File not found at $file_path"fiRepository: cemililik/LearnStack
Length of output: 1279
Restrict over-permissive settings on learnstack-api client.
For a confidential backend client, enabling direct grants, wildcard web origins, and full scope allowance materially expands attack surface without clear need. Keycloak 26.x documentation and maintainers recommend disabling these for production-grade service-account backends: directAccessGrantsEnabled (deprecated in OAuth 2.1), webOrigins (unnecessary for backend-only clients), and fullScopeAllowed (dev-only per official service-account docs).
🔒 Suggested hardening baseline
- "directAccessGrantsEnabled": true,+ "directAccessGrantsEnabled": false,
@@
- "webOrigins": ["+"],+ "webOrigins": [],
@@
- "fullScopeAllowed": true+ "fullScopeAllowed": false🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@infra/keycloak/realms/learnstack.json` around lines 68 - 75, The
learnstack-api client config is over-permissive: disable direct access grants by
setting "directAccessGrantsEnabled" to false (remove deprecated direct grant use
for a confidential service-account), replace the wildcard webOrigins entry
("webOrigins": ["+"]) with an empty array or explicit origins (e.g., []) so
backend-only clients have no permissive CORS origins, and ensure
"fullScopeAllowed" is explicitly set to false for the client (add
"fullScopeAllowed": false) to prevent automatic granting of all roles/scopes to
the service account; keep "serviceAccountsEnabled": true and only whitelist
necessary "redirectUris" if required.
Triaged 28 findings. 27 applied verbatim; F7 (`learnstack-hub-web` fullScopeAllowed=true) intentionally kept with inline dev-posture comment because the reviewer's full fix (`fullScopeAllowed=false` + explicit scopeMappings) would break the demo-operator login flow in dev — production realm provisioning is a separate flow and is the right place to harden public clients with least-privilege scope maps. Blockers / real bugs - `Testcontainers.Valkey` doesn't exist on NuGet — sed-sweep collateral damage from the Redis→Valkey doc pass. Reverted to `Testcontainers.Redis` in both `Directory.Packages.props` and `LearnStack.Tests.Integration.csproj`, with an inline DO-NOT-RENAME comment explaining the protocol-vs-vendor split (the Testcontainers .NET library publishes its RESP-compatible test container under the `Testcontainers.Redis` identifier; the container itself can target any RESP-compatible image including Valkey). - Host port 8080 collision between SeaweedFS volume HTTP API and Keycloak. Moved SeaweedFS volume API to host port 8084 (container port 8080 unchanged); updated `infra/seaweedfs/README.md` accordingly. - ADR-0002 Decision + Consequences sections were edited in 3fd9054 in violation of CLAUDE.md's "never edit an Accepted ADR's decision section" rule. Reverted both sections to their original 2026 wording (PostgreSQL / Redis / MinIO); kept the Status amendment block and the two dated Amendment blocks at the bottom; added a note in the Status section explaining the immutability + amendment-pattern split. Security hardening (Keycloak realms) - `learnstack-api` (confidential service): `directAccessGrantsEnabled` flipped to false (Direct Access Grants = deprecated OIDC ROPC, not needed for the .NET API's standard-flow + client_credentials usage); `webOrigins` tightened to `[]` (backend-only client, CORS is handled at apps/web + APISIX). `fullScopeAllowed` kept true with an inline `_devNote_fullScopeAllowed` field explaining the dev-vs-production posture. - `learnstack-web` + `learnstack-hub-web` (public PKCE): inline dev- posture note added to each client's `description` field, pointing production realm provisioning at the explicit-scopeMappings pattern. Doc accuracy - Cost model (`08-livekit-cost-model.md`): "1 TB recording storage" was mathematically wrong against the document's own 60 MB/min assumption for 300,000 minutes. Corrected to ~18 TB + Hetzner BX21-tier €70/mo ballpark; total Hetzner monthly bumped from $650-800 to $720-870. - Standards 20 TTL guidance: split the "in-process / Valkey layer 60s" one-liner into explicit L1 (in-process `IMemoryCache` = 60s) and L2 (Dapr state → Valkey = 15-min upper bound) clauses. Same split applied in the `add-feature-key` skill's pitfall section. - Standards 05 + Standards 20: added ADR-0029 / ADR-0030 / ADR-0031 to the `Derives from:` provenance headers. - ADR-0014 component file table: corrected `dapr/components/pubsub.yaml` → `infra/dapr/components/pubsub-kafka.yaml` (+ statestore-redis.yaml + secretstore-vault.yaml), with a note that the `-redis` suffix is the Dapr provider-type convention, not vendor branding. - `architecture/29-dapr-integration.md` statestore section: added a callout that `state.redis` + `redisHost` are Dapr / RESP-protocol identifiers, not vendor markers; runtime points at the `valkey` service per ADR-0030. - `architecture/04-technical-architecture.md`: "PostgreSQL 18+" → "PostgreSQL 18.x" (major-pin policy per ADR-0031); "Valkey 7+ via Dapr State Store" → "Valkey 8.x via Dapr State Store" with citation; stale `redis` line in Local Infrastructure list → `valkey`. - ADR-0002 + ADR-0031 ADR-0023 link targets: `README.md` → `README.md#open-adr-drafts` (3 places) since ADR-0023 is a reserved draft listed in the decisions index, not a standalone file. - Stale Phase-01 status text refreshed in CLAUDE.md, README.md, `roadmap/phase-01-repository-tooling.md`, and `Standards 12 § Local Infrastructure` compose list (removed `apisix-dashboard` + `redis` references; added Valkey / SeaweedFS / Postgres 18 ADR pointers). - `local-dev-setup` skill: deployment-mode table now uses `SelfHostedOnline` / `SelfHostedAirGapped` (the current model from Standards 12) instead of the obsolete `SelfHosted`; removed the APISIX dashboard row from the dev port table. Hygiene - Kafka KRaft `CLUSTER_ID` switched from the freeform string `learnstack-dev-cluster-id-1` to a real URL-safe Base64-encoded UUID (`ofPH0p5rSlK52BxOX3qLPQ`) matching the format `kafka-storage.sh random-uuid` emits. - Markdown lint (MD040) fence-language fixes: `text` added to four fenced blocks (`Standards 06` test-pyramid Mermaid + text fallback; `architecture/09-tenant-isolation.md` storage example; `infra/dapr/README.md` sidecar topology ASCII; `infra/apisix/README.md` plugin chain ASCII). Skipped with reason - **F7 (learnstack-hub-web fullScopeAllowed=true → false + explicit scopeMappings)**: the full fix would block demo-operator from obtaining a hub-operator role claim in dev, breaking the operator portal demo flow. Production realm provisioning (separate from this dev seed JSON, per `infra/keycloak/README.md`) is the right place to apply least-privilege scope maps. The kept-true value carries an inline note pointing at that. Verification - `docker compose -f infra/compose/dev.yml config -q` exits 0. - Both Keycloak realm JSONs parse cleanly. - No `Testcontainers.Valkey` references remain outside the DO-NOT-RENAME explanatory comment. - Only Keycloak claims host port 8080; SeaweedFS volume API now on 8084. - ADR-0002 Decision section reads "PostgreSQL, Redis, MinIO" verbatim (the original); current-state guidance lives in the two dated Amendment blocks at the bottom. - All three ADR-0023 links point at `README.md#open-adr-drafts`. - Kafka CLUSTER_ID is a valid 22-char URL-safe Base64 UUID. - Markdown link sweep on every changed doc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-2 review surfaced four runtime blockers and a handful of doc / config drifts. Round-1 fixes already covered most static issues; these are the live-smoke and post-bump alignment items. Live-smoke blockers (compose now reaches "all healthy"): - Postgres 18 refuses any mount at /var/lib/postgresql/data — the modern layout mounts the parent /var/lib/postgresql and lets the image place data at $PGDATA. Old `postgres-data` volumes must be wiped (`docker volume rm`); compose note added. - LiveKit's built-in TURN restart-looped on `domain: localhost`. Turn it off in dev — ADR-0005 uses standalone Coturn anyway. - APISIX 3.16-debian ships neither curl nor wget. Probe via bash /dev/tcp; raise start_period to 30s. - SeaweedFS + Meilisearch healthchecks failed under `localhost` (image resolvers prefer ::1). Pin probes to 127.0.0.1. Image pin policy: - kafka-ui was the lone `:latest` in the stack. Pin to v1.5.0 (current kafbat release). Identity-realm posture: - learnstack accessTokenLifespan 3600 → 1800 to match learnstack-hub (dev fixtures internally consistent; production provisioning sets 900 anyway). - README documents the tenant-realm dev-permissive defaults (registrationAllowed=true, verifyEmail=false, 1800s) so production provisioning flips each deliberately. - Long inline client descriptions broke realm import (Keycloak CLIENT.DESCRIPTION is varchar(255)); short descriptions + central README posture section instead. Standards / docs drift: - Standards 11 / 12 / 20 + phase-02a-kernel-tenancy described APISIX mTLS as a route plugin. APISIX 3.x has no `mtls` plugin; mTLS is SSL-object config (client.ca / client.depth) plus a route-level ip-restriction. Rewritten to match the canonical stub in infra/apisix/apisix.yaml. - Standards 12 § Local Infrastructure listed livekit-egress and otel-collector as present; they are Phase 08c / Phase 11. Split into "Shipped in Phase 01" and "Deferred" lists. - Standards 12 Derives-from now cites ADR-0029 / 0030 / 0031. - ADR-0029 image sentence said `chrislusf/seaweedfs:latest (pinned)` — contradiction with the rule and with dev.yml's 3.94. Reworded. - ADR index "ADR-0002's Valkey row" was always Redis. Fixed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
docs/standards/11-security.md (1)
57-72: ⚡ Quick winAdd language specifiers to fenced code blocks.
The code blocks at lines 57 and 76 should include a language identifier for better rendering and tooling support. For service lists like these, use
```textor```yamlif the content will become YAML, or```shif they're meant to be command-like.📝 Proposed fix
For line 57:
-```+```text postgres # PostgreSQL 18.x per ADR-0031For line 76:
-```+```text livekit-egress # Phase 08c (recording / consent / cost model)Based on learnings: static analysis tool flagged this as MD040 (fenced-code-language).
Also applies to: 74-79
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/standards/11-security.md` around lines 57 - 72, Add a language specifier to the fenced code blocks that list services (e.g., the blocks containing the lines starting with "postgres # PostgreSQL 18.x per ADR-0031" and "livekit-egress # Phase 08c (recording / consent / cost model)") by changing their opening fences to include a language like "text" (or "yaml" if the block will be parsed as YAML, or "sh" if command-like); apply the same change to the other nearby service list block(s) flagged (the block covering the "livekit-egress" entry and the surrounding 74-79 range) so all fenced code blocks include a language identifier to satisfy MD040 and improve rendering/tooling.infra/dapr/README.md (1)
23-37: 💤 Low valueConsider Mermaid diagram for better maintainability.
The ASCII art effectively shows the sidecar topology, but the coding guideline requires Mermaid for diagrams. A Mermaid flowchart or graph would be more maintainable and render consistently across viewers.
♻️ Example Mermaid conversion
-```text-┌──────────────────────────┐ ┌──────────────────────────────────────────┐-│ dotnet run │ │ daprd │-│ → host:5080 │ │ ./daprd -app-id learnstack-api \ │-│ │ │ -app-port 5080 \ │-│ │◄──┤ -app-channel-address \ │-│ │ │ host.docker.internal \ │-│ │ │ -dapr-http-port 3500 \ │-│ │ │ -dapr-grpc-port 50001 \ │-│ │ │ -placement-host-address \ │-│ │ │ dapr-placement:50005 \ │-│ │ │ -resources-path /components \ │-│ │ │ -config /config/dapr-config.yaml-└──────────────────────────┘ └──────────────────────────────────────────┘-```+```mermaid+graph LR+ A["dotnet run<br/>→ host:5080"]+ B["daprd<br/>-app-id learnstack-api<br/>-app-port 5080<br/>-app-channel-address host.docker.internal<br/>-dapr-http-port 3500<br/>-dapr-grpc-port 50001<br/>-placement-host-address dapr-placement:50005<br/>-resources-path /components<br/>-config /config/dapr-config.yaml"]+ B -->|subscribes| A+```Based on learnings: Use Mermaid for diagrams in fenced code blocks (per coding guidelines for
**/*.md).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@infra/dapr/README.md` around lines 23 - 37, Replace the ASCII art diagram block with a Mermaid diagram fenced code block that models the same sidecar topology: create a left node for the application ("dotnet run → host:5080") and a right node for the dapr sidecar including the CLI flags shown (e.g., -app-id learnstack-api, -app-port 5080, -app-channel-address host.docker.internal, -dapr-http-port 3500, -dapr-grpc-port 50001, -placement-host-address dapr-placement:50005, -resources-path /components, -config /config/dapr-config.yaml) and draw a directed link from the dapr node to the app node; ensure the Mermaid block uses graph LR and preserves the same text/flags from the original ASCII art so the diagram remains accurate and maintainable.infra/coturn/turnserver.conf (1)
7-16: ⚡ Quick winCoturn TURN server integration deferred to Phase 08c —
external-ipand credential-passing not yet implemented.The concern about unroutable relay addresses is technically valid for Coturn's general behavior, but the current dev setup is placeholder infrastructure. The
livekit.yamlcomment explicitly states thatrtc.turn_servers(which passes Coturn credentials to clients) lands in Phase 08c; until then, Coturn is not integrated at all, so the lack ofexternal-ipis not a blocker.Additionally, a static
external-ipin the config is insufficient for a multi-developer localhost setup — developers testing symmetric-NAT relay must switch--node-ipfrom127.0.0.1to their LAN IP anyway, which meansexternal-ipalso needs to be dynamic or environment-injected. The proper fix is part of the Phase 08cILiveClassProviderwiring that includes per-session credential issuance and ICE server configuration, not a static value inturnserver.conf.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@infra/coturn/turnserver.conf` around lines 7 - 16, Update the turnserver.conf comment to explicitly state that external-ip and credential injection are deferred to Phase 08c (so no static external-ip is set here), and reference the planned work items: rtc.turn_servers in livekit.yaml and ILiveClassProvider for per-session credential issuance; also mention developer-local testing requires setting --node-ip to the machine LAN IP (so external-ip must be dynamic/env-injected later). This makes it clear the lack of external-ip is intentional and will be implemented as part of Phase 08c rather than in this static config.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@infra/dapr/README.md`:
- Around line 10-12: Update the README to remove ambiguity around phase
assignments by explicitly stating whether ICacheService and ISecretProvider
interfaces are defined in Phase 02a but their implementations (Valkey and Vault)
ship in Phase 02b; if that is the case add a short note near the building blocks
table and at the sentences around lines referencing "all three implementations
ship in Phase 02b" clarifying that interfaces (ICacheService, ISecretProvider)
are available as stubs/mocks or as interface-only contracts in 02a and full
implementations arrive in 02b, and if instead all three should read Phase 02b,
change the table entries for ICacheService and ISecretProvider to Phase 02b so
table and paragraph match.
---
Nitpick comments:
In `@docs/standards/11-security.md`:
- Around line 57-72: Add a language specifier to the fenced code blocks that
list services (e.g., the blocks containing the lines starting with "postgres # PostgreSQL 18.x per ADR-0031" and "livekit-egress # Phase 08c
(recording / consent / cost model)") by changing their opening fences to include
a language like "text" (or "yaml" if the block will be parsed as YAML, or "sh"
if command-like); apply the same change to the other nearby service list
block(s) flagged (the block covering the "livekit-egress" entry and the
surrounding 74-79 range) so all fenced code blocks include a language identifier
to satisfy MD040 and improve rendering/tooling.
In `@infra/coturn/turnserver.conf`:
- Around line 7-16: Update the turnserver.conf comment to explicitly state that
external-ip and credential injection are deferred to Phase 08c (so no static
external-ip is set here), and reference the planned work items: rtc.turn_servers
in livekit.yaml and ILiveClassProvider for per-session credential issuance; also
mention developer-local testing requires setting --node-ip to the machine LAN IP
(so external-ip must be dynamic/env-injected later). This makes it clear the
lack of external-ip is intentional and will be implemented as part of Phase 08c
rather than in this static config.
In `@infra/dapr/README.md`:
- Around line 23-37: Replace the ASCII art diagram block with a Mermaid diagram
fenced code block that models the same sidecar topology: create a left node for
the application ("dotnet run → host:5080") and a right node for the dapr sidecar
including the CLI flags shown (e.g., -app-id learnstack-api, -app-port 5080,
-app-channel-address host.docker.internal, -dapr-http-port 3500, -dapr-grpc-port
50001, -placement-host-address dapr-placement:50005, -resources-path
/components, -config /config/dapr-config.yaml) and draw a directed link from the
dapr node to the app node; ensure the Mermaid block uses graph LR and preserves
the same text/flags from the original ASCII art so the diagram remains accurate
and maintainable.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b1dfbc63-0a72-4eb6-b636-8c61eb18641e
📒 Files selected for processing (32)
.claude/skills/add-feature-key/SKILL.md.claude/skills/local-dev-setup/SKILL.mdCLAUDE.mdREADME.mdbackend/Directory.Packages.propsdocs/architecture/04-technical-architecture.mddocs/architecture/08-livekit-cost-model.mddocs/architecture/09-tenant-isolation.mddocs/architecture/29-dapr-integration.mddocs/decisions/0002-initial-architecture.mddocs/decisions/0014-adopt-dapr.mddocs/decisions/0029-object-storage-seaweedfs.mddocs/decisions/0031-postgresql-major-version.mddocs/decisions/README.mddocs/roadmap/phase-01-repository-tooling.mddocs/roadmap/phase-02a-kernel-tenancy.mddocs/standards/05-database.mddocs/standards/06-testing.mddocs/standards/11-security.mddocs/standards/12-infrastructure.mddocs/standards/20-infrastructure-stack.mdinfra/apisix/README.mdinfra/compose/README.mdinfra/compose/dev.ymlinfra/coturn/turnserver.confinfra/dapr/README.mdinfra/dapr/components/secretstore-vault.yamlinfra/keycloak/README.mdinfra/keycloak/realms/learnstack-hub.jsoninfra/keycloak/realms/learnstack.jsoninfra/livekit/livekit.yamlinfra/seaweedfs/README.md
✅ Files skipped from review due to trivial changes (17)
- docs/roadmap/phase-02a-kernel-tenancy.md
- docs/standards/06-testing.md
- CLAUDE.md
- docs/architecture/09-tenant-isolation.md
- backend/Directory.Packages.props
- docs/decisions/README.md
- docs/decisions/0002-initial-architecture.md
- docs/architecture/08-livekit-cost-model.md
- infra/keycloak/README.md
- README.md
- docs/decisions/0029-object-storage-seaweedfs.md
- infra/apisix/README.md
- docs/decisions/0031-postgresql-major-version.md
- docs/roadmap/phase-01-repository-tooling.md
- infra/compose/README.md
- docs/architecture/29-dapr-integration.md
- docs/architecture/04-technical-architecture.md
🚧 Files skipped from review as they are similar to previous changes (6)
- docs/standards/05-database.md
- infra/keycloak/realms/learnstack-hub.json
- infra/keycloak/realms/learnstack.json
- docs/decisions/0014-adopt-dapr.md
- infra/dapr/components/secretstore-vault.yaml
- docs/standards/20-infrastructure-stack.md
| | Pub/Sub | Kafka (`kafka:9092`) | `components/pubsub-kafka.yaml` | `IEventBus` (Phase 02b) | | ||
| | State store | Valkey (`valkey:6379`, RESP protocol) | `components/statestore-redis.yaml` | `ICacheService` (Phase 02a) | | ||
| | Secret store | Vault (`http://vault:8200`, dev mode) | `components/secretstore-vault.yaml` | `ISecretProvider` (Phase 02a) | |
There was a problem hiding this comment.
Clarify phase assignments for cache and secret abstractions.
The building blocks table indicates ICacheService and ISecretProvider are available in Phase 02a, but lines 64 and 96-97 state all three implementations ship in Phase 02b. This creates ambiguity: are the interfaces defined in 02a but unimplemented until 02b, or should the table show 02b for all three?
If interfaces are genuinely split from implementations across phases, please add a note explaining the gap and what (if any) stub/mock is used in 02a.
Also applies to: 96-97
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@infra/dapr/README.md` around lines 10 - 12, Update the README to remove
ambiguity around phase assignments by explicitly stating whether ICacheService
and ISecretProvider interfaces are defined in Phase 02a but their
implementations (Valkey and Vault) ship in Phase 02b; if that is the case add a
short note near the building blocks table and at the sentences around lines
referencing "all three implementations ship in Phase 02b" clarifying that
interfaces (ICacheService, ISecretProvider) are available as stubs/mocks or as
interface-only contracts in 02a and full implementations arrive in 02b, and if
instead all three should read Phase 02b, change the table entries for
ICacheService and ISecretProvider to Phase 02b so table and paragraph match.
Uh oh!
There was an error while loading. Please reload this page.
Three round-3 nitpicks against the Phase 01 packets 4-6 surface. infra/dapr/README.md: - Phase-assignment ambiguity. The building-blocks table said `ICacheService` / `ISecretProvider` ship in 02a but `IEventBus` in 02b, while a later paragraph claimed all three Dapr-backed implementations ship in 02b. The canonical answer (per phase-02a § Shared Kernel + § Dapr Building Blocks) is that ALL three interfaces AND their Dapr-backed implementations ship in 02a; only the OutboxProcessor (the consuming call site for IEventBus) waits until 02b. Replaced the per-row "(Phase 02a/02b)" hint with a dedicated phase-ownership block and corrected the trailing paragraph. - Sidecar-topology ASCII art replaced with a Mermaid `graph LR` block per CLAUDE.md hard rule (diagrams use Mermaid; text fallback retained for non-Mermaid renderers per the same rule). infra/coturn/turnserver.conf: - The deliberate omission of `external-ip`, the still-absent `rtc.turn_servers` block in livekit.yaml, and the static user/secret pair are all Phase 08c work. Added a "DEFERRED to Phase 08c" header pointing at the three concrete follow-ups (external-ip injection, livekit-side turn_servers wiring, ILiveClassProvider per-session credential minting via `use-auth-secret`). docs/standards/12-infrastructure.md: - MD040: the two service-list fenced code blocks were unmarked. Added the `text` language identifier to both so the standard renders consistently and the markdown linter is happy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…estion Code-review agent flagged eight findings on bb0cf16 (ADR-0032 + cross-cutting concerns). Aksiyon haritası: Major ----- - Major #1: TenantContextSpanProcessor would fail at startup with "Cannot consume scoped service ITenantContext from singleton" if the constructor takes the request-scoped ITenantContext directly. Introduce ITenantContextAccessor (singleton, AsyncLocal<ITenantContext?>-backed, analogous to IHttpContextAccessor). The scoped interface remains for handler-facing code; the singleton accessor is what cross-cutting infrastructure (OTel processor, Serilog enricher, Sentry enricher) reads. The accessor is set at scope start by TenantResolverMiddleware, HubCorrelationMiddleware, Hangfire JobActivator, and the outbox / inbox handler scope. Phase 02a ships both contracts together. Updated: ADR-0032 § Sub-decision 10 + § Implementation Notes (TenantContextSpanProcessor shape), wire-cross-cutting-foundation skill (new Step 4 + accessor pattern), Standards 10 § Span Attributes, Phase 02a roadmap deliverables, Architecture 33 § Tracing Stack, glossary entry. - Major #2: ADR-0032 § Sub-decision 5 originally listed "Hub HTTP clients" among IProviderResilience<TPort>-wired adapters and carried a Resilience.hub: row in the appsettings example. The new add-provider-adapter skill correctly excludes Hub adapters because they have an additional mTLS + signed JWT + HMAC wrapper per ADR-0019. The two documents disagreed. Resolve by removing "Hub HTTP clients" from the adapter list and dropping the appsettings example row — Hub adapters get their resilience inside the ADR-0019 wrapper, defined when the Hub adapter itself lands in Phase 02c. Minor ----- - Minor #3: Standards 09 introductory Mermaid diagram still labelled "Global exception middleware" — replaced with "L1 IExceptionHandler" to match the rewritten section below. - Minor #4: "(200 OK on response = success at runtime)" parenthetical in the Sentry/OTel partition table conflated HTTP status (Result.Fail returns 4xx) with OTel ActivityStatusCode. Reworded to "runtime completed; HTTP response is the appropriate 4xx Problem Details". - Minor #5: Standards 09 listed a custom UnreachableException subclass that collides with System.Diagnostics.UnreachableException (.NET 7+). Dropped the subclass; standardized on the BCL type. Skill snippet qualified. - Minor #6: ADR-0032 was missing the Deciders: line that ADR-0029/30/31 carry. Added "@platform". - Minor #8: Standards 10 § Correlation table omitted organization_id even though sub-decision 10 binds the processor to enrich every span with it. Added the row, cited ADR-0017. - Minor #9: The eight-row Sentry/OTel partition table was duplicated in ADR-0032 § Sub-decision 7 and Standards 09 § Sentry vs OpenTelemetry. Two copies will drift. Kept the authoritative table in Standards 09; ADR-0032 now carries a compact summary and cites the standard. Suggestion ---------- - Suggestion #10: Renamed the architecture test Pipeline_Order_Matches_ADR_0032 → MediatR_Pipeline_Order_Matches_Canonical_Sequence in all five reference sites (ADR-0032, wire-cross-cutting-foundation, glossary, phase-02a roadmap, Standards 02). The ADR citation moves to the test's [Description] attribute at implementation time. Embedding the ADR number in the assembly-level identifier would bake a version into the test name; the Subject_Constraint naming form already used by the rest of the architecture-test set (Modules_Do_Not_Reference_DeploymentMode, Every_TenantOwned_Command_HasAuditCoverage) is the correct pattern. Not addressed (intentional) --------------------------- - Minor #7: The prior commit body (bb0cf16) understated the CLAUDE.md rule count ("six" vs eight added). The body is the durable record; amending it would rewrite history. Noted here for future readers. ADR: 0032 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
the data-plane scaffolding already on
main: Keycloak two-realm identity(packet 4), LiveKit OSS + Coturn live-media (packet 5), and Kafka + Vault
MinIO/Redis/Postgresrow in ADR-0002 originally and now have their owndecision records: ADR-0029 SeaweedFS (MinIO repo archived; license
trajectory removed the no-phone-home Self-Hosted Air-Gapped path),
ADR-0030 Valkey (Redis 7.4 was the last BSD-3-Clause release; Valkey
is the Linux-Foundation-governed RESP-protocol drop-in), ADR-0031
PostgreSQL 18 (longest LTS runway, native
gen_uuid_v7()that theADR-0023 draft can adopt without an extension, async I/O for
audit_logscans). ADR-0002 gets dated Amendment 1 + 2 without its Decision section
being rewritten.
stable (Postgres 18.4, Valkey 8.1, SeaweedFS 3.94, Mailpit 1.29.7,
Meilisearch 1.44, Keycloak 26.6.2, LiveKit 1.12, Coturn 4.11, Kafka CP
8.2.1, Vault 1.21.4, Dapr 1.17.7, APISIX 3.16);
kafka-uiswapped fromthe abandoned
provectuslabsfork to the activekafbat/kafka-uifork.collision (resolved by removing the dashboard companion when APISIX moved
to file-driven
data_planestandalone), Keycloak healthcheck (curl notin image → bash
/dev/tcpprobe), Dapr sidecar-app-channel-address host.docker.internal(subscription deliveries to workstation API),APISIX
mtlsroute-plugin removed (mTLS in APISIX is SSL-object config,not a route plugin),
statestore-redis.yamlactorStateStore: "false"(ADR-0014 actors out of scope), Keycloak-hub demo operator
requiredActions: ["CONFIGURE_TOTP"]so the MFA flow surfaces.What's on the branch
73 files, +2,313 / −218.
What does NOT change (worth flagging up front for reviewers)
IStorageProvider/ICacheService/IEventBus/ISecretProvidercontracts — none of these exist as code yet (Phase 02a). The ADRs are
forward-looking; the adapter implementations will sit behind the same
interfaces ADR-0014 defined.
StackExchange.Redis,IConnectionMultiplexer,state.redisDaprcomponent,
Microsoft.Extensions.Caching.Redis,RedisCacheServiceexample names. The Redis→Valkey doc sweep treats these as RESP / .NET
type identifiers, not vendor brands.
app.tenant_id/app.organization_idsessionvars — unchanged from Postgres 16 → 18, so the ADR-0003 defense-in-
depth pattern transfers verbatim.
data_planemode explicitly excludes the Admin API and thecompanion dashboard per ADR-0015's "standalone, no etcd" commitment.
apisix.yamlis now the only source of truth for routes; diff-reviewreplaces the dashboard for dev.
Deliberately deferred to later packets / phases
make devorchestrator,.env.example, pre-commit hook,e2e.ymlcompanion stack → Phase 01 packet 7 (DX).
make seedwith two demo tenants →Phase 01 packet 8 (CI + seed).
IEventBus/ICacheService/ISecretProvider/IStorageProvideradapter implementations + the outbox + the
IInboxGuard→ Phase 02b.Phase 11 (production hardening).
Test plan
docker compose -f infra/compose/dev.yml config -qexits 0 (alreadygreen locally — please confirm on a fresh clone).
docker compose -f infra/compose/dev.yml up -dbrings every serviceto a
healthy(orrunningfor services without a probe) state within~90s. Keycloak's first boot is the slowest at ~60s. (Live
upsmokewas not run locally — Docker daemon unreachable on the workstation that
produced these commits.)
localhost:5432(
psql -h localhost -U learnstack -d learnstack -c '\\dt').localhost:6379(
valkey-cli pingreturnsPONG).localhost:9000with credentialslearnstack/learnstack-dev-secret; filer UI onlocalhost:9001.localhost:8025; Meilisearch onlocalhost:7700.localhost:8080withadmin/admin-dev-secret; bothlearnstack+learnstack-hubrealmsimported.
ws://localhost:7880; Coturn onlocalhost:3478.localhost:8081; Vault statusOK on
localhost:8200with tokenlearnstack-dev-root-token.localhost:3500/v1.0/healthz; metadataendpoint lists
pubsub+statestore+secretstorecomponentsloaded.
localhost:9080; Prometheus metrics onlocalhost:9091.backend code changed, but a sanity check on a fresh clone is cheap).
git grep -nE '\\bMinIO\\b|minio'returns only the 5 intentionalhistorical-context sites (ADR-0002, ADR-0029, infra/seaweedfs/README,
infra/compose/dev.yml inline comment, infra/compose/README.md).
git grep -nE '\\bRedis\\b'returns only the 6 intentional sites(ADR-0002, ADR-0030, ADR-0014 library refs, ADR-0029, dev.yml inline
comment, statestore-redis.yaml inline comment + library refs in
architecture/29-dapr-integration.md).
gh pr viewconfirms no ADR-0002 Decision section edits — onlyAmendment-block additions.
🤖 Generated with Claude Code
Summary by CodeRabbit
Documentation
Chores