Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@ index plus the relevant [Architecture](../../docs/architecture/) doc.

| Skill | When to use |
|-------|-------------|
| [local-dev-setup](local-dev-setup/SKILL.md) | Bringing up the local stack: Postgres, Redis, Vault, Kafka, Dapr sidecar, APISIX, Keycloak, MinIO, LiveKit, Meilisearch. |
| [local-dev-setup](local-dev-setup/SKILL.md) | Bringing up the local stack: Postgres, Valkey, Vault, Kafka, Dapr sidecar, APISIX, Keycloak, SeaweedFS, LiveKit, Meilisearch. |
| [seed-tenant](seed-tenant/SKILL.md) | Provisioning a tenant (with its default organization, customization data, seed users) for local development. |

## Authoring a new skill
Expand Down
8 changes: 5 additions & 3 deletions .claude/skills/add-feature-key/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -265,9 +265,11 @@ public async Task LimitKey_Soft_SurfaceBanner_DoesNotBlock() { ... }
- **Writing a plan-projected key to `tenant_feature_flags`.** Architecture test
rejects. Plan keys belong to the entitlement projection only.
- **Reading the key from raw SQL.** Forbidden; use `IFeatureFlags`.
- **Hot path without Redis cache.** Each `IsEnabledAsync` call could become
DB-bound. The Redis layer (60s TTL, eager-invalidated by Dapr event) is
load-bearing.
- **Hot path without the cache stack.** Each `IsEnabledAsync` call could become
DB-bound. The two cache layers (L1 in-process `IMemoryCache` = 60s TTL;
L2 Dapr state → Valkey = 15-min upper bound; both eager-invalidated by the
`learnstack.hub.entitlement` Dapr event) are load-bearing — see Standards 20
§ Configuration / Eager invalidation.
- **Killswitch without runbook.** The runbook is part of the deliverable. CI does
not enforce its presence today; review must.
- **Removing a key without a deprecation cycle.** A rename / remove follows the
Expand Down
8 changes: 4 additions & 4 deletions .claude/skills/add-integration-test/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ name: add-integration-test
description: >
Write a Testcontainers-backed integration test in
`backend/tests/LearnStack.Tests.Integration` that exercises a real Postgres +
Redis + (optionally) Dapr stack and asserts behaviour under tenant + organization
Valkey + (optionally) Dapr stack and asserts behaviour under tenant + organization
context. USE FOR: cross-tenant / cross-org isolation tests (mandatory for every
new `[TenantOwned]` / `[OrganizationScoped]` entity), outbox → consumer round
trips, audit-pipeline assertions, RLS-effective-isolation tests. DO NOT USE FOR:
Expand DownExpand Up@@ -31,7 +31,7 @@ architecture test) plus any other invariant the change touches. See
- Outbox → consumer round-trip.
- Audit-pipeline-writes-the-expected-row.
- Provider-adapter contract test that talks to a containerised real provider
(LiveKit OSS, MinIO, Meilisearch).
(LiveKit OSS, SeaweedFS, Meilisearch).

## When not to use

Expand All@@ -45,7 +45,7 @@ architecture test) plus any other invariant the change touches. See
|-------|----------|-------------|
| Scenario | Yes | A short name + setup + act + assert. |
| Seed | Yes | Minimum tenants / orgs / users / customization data the scenario needs. |
| Required containers | Yes | Postgres always; add Redis / Kafka / Meilisearch / LiveKit / MinIO as needed. |
| Required containers | Yes | Postgres always; add Valkey / Kafka / Meilisearch / LiveKit / SeaweedFS as needed. |
| Tenant context | Yes | Which tenant + org the act phase runs as. |

## Workflow
Expand All@@ -54,7 +54,7 @@ architecture test) plus any other invariant the change touches. See

The project ships `TestFixture` that:

- Spins Postgres + Redis (+ optional Kafka via Dapr) via Testcontainers.
- Spins Postgres + Valkey (+ optional Kafka via Dapr) via Testcontainers.
- Applies all migrations (per module).
- Seeds a baseline platform admin, two tenants, two orgs per tenant.
- Exposes `fixture.AsTenant(tenantId, organizationId?)` to scope a block.
Expand Down
26 changes: 13 additions & 13 deletions .claude/skills/local-dev-setup/SKILL.md
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
---
name: local-dev-setup
description: >
Bring up the LearnStack local stack — Postgres, Redis, Vault, Kafka, Dapr
sidecar, APISIX, Keycloak (two realms), MinIO, LiveKit OSS, Meilisearch — via
Bring up the LearnStack local stack — Postgres, Valkey, Vault, Kafka, Dapr
sidecar, APISIX, Keycloak (two realms), SeaweedFS, LiveKit OSS, Meilisearch — via
`docker-compose` plus the project's `make dev` orchestrator. USE FOR: first-time
workstation setup, restoring a broken local environment, switching between
`DeploymentMode` for testing. DO NOT USE FOR: production deployment (separate
Expand All@@ -15,7 +15,7 @@ description: >
## Purpose

Stand up a full LearnStack stack on a developer workstation so backend + frontend
can run against real Postgres / Redis / Kafka / Vault / Keycloak / MinIO /
can run against real Postgres / Valkey / Kafka / Vault / Keycloak / SeaweedFS /
LiveKit / Meilisearch / APISIX — the same components production uses
([12-infrastructure.md § Local Infrastructure](../../../docs/standards/12-infrastructure.md),
[20-infrastructure-stack.md](../../../docs/standards/20-infrastructure-stack.md)).
Expand DownExpand Up@@ -45,7 +45,7 @@ LiveKit / Meilisearch / APISIX — the same components production uses
| Docker Desktop | Yes | Required for every container. |
| .NET 10 SDK | Yes | `dotnet --version` returns `10.0.x`. |
| Node 20+ + pnpm | Yes | For the frontend. |
| Deployment mode | Yes | `Development` (default) / `SaaS` / `Dedicated` / `SelfHosted`. |
| Deployment mode | Yes | `Development` (default) / `SaaS` / `Dedicated` / `SelfHostedOnline` / `SelfHostedAirGapped` (per [Standards 12 § Deployment Modes](../../../docs/standards/12-infrastructure.md)). |
| `.env` (gitignored) | Optional | Local overrides; `.env.example` is the source of truth. |

## Workflow
Expand DownExpand Up@@ -98,21 +98,20 @@ The components and their default ports:
| Component | Port | Purpose |
|-----------|------|---------|
| Postgres | 5432 | Main DB. |
| Redis | 6379 | Cache + Dapr state. |
| Valkey | 6379 | Cache + Dapr state. |
| Kafka | 9092 | Dapr pub/sub backend. |
| Kafka UI | 9094 | Optional UI for topics. |
| Vault (dev mode) | 8200 | Secrets backend; `root` token, **not** for production. |
| Keycloak | 8080 | Two realms: `learnstack` (tenants) + `learnstack-hub` (operators). |
| MinIO | 9000 (API) / 9001 (console) | Object storage. |
| SeaweedFS | 9000 (S3 API) / 9001 (filer UI) / 9333 (master) | Object storage (single dev binary: master + volume + filer + S3 gateway). |
| Meilisearch | 7700 | Search. |
| LiveKit | 7880 (API) / 7881 (TLS) / 7882 (RTC) | Live classroom. |
| coturn | 3478 / 5349 | TURN for LiveKit. |
| LiveKit Egress | — | Recording worker. |
| Mailhog | 8025 | Captures outbound email in dev. |
| OTel Collector | 4317 (gRPC) | Local observability. |
| Dapr sidecar (per service) | 3500 (HTTP) / 50001 (gRPC) | Building-block runtime. |
| APISIX | 9080 (gateway) / 9180 (admin) | API gateway. |
| APISIX dashboard (optional) | 9000 | Route inspection. |
| APISIX | 9080 (HTTP gateway) / 9443 (HTTPS) / 9091 (Prometheus metrics) | File-driven standalone (`data_plane`) per ADR-0015 — no Admin API, no dashboard companion. `apisix.yaml` is the only source of truth. |
| LearnStack API | 5100 | Backend host. |
| LearnStack Web (`apps/web`) | 3000 | Frontend dev server. |

Expand All@@ -128,7 +127,7 @@ The first `make dev` run additionally:
4. Seeds the two demo tenants' customization data (`TenantContentType`,
`TenantPageBlock`, `TenantLevelTaxonomy`, …) so the page renderer has
something to render.
5. Creates the MinIO buckets.
5. Creates the SeaweedFS buckets.
6. Creates the Meilisearch indexes.

For a clean re-seed:
Expand All@@ -150,8 +149,8 @@ curl -fsS http://localhost:9080/healthz | jq
open http://localhost:8080/realms/learnstack/account
open http://localhost:8080/realms/learnstack-hub/account

# MinIO console
open http://localhost:9001 # minioadmin / minioadmin
# SeaweedFS filer UI (replaces the MinIO console of the prior stack)
open http://localhost:9001 # S3 access: learnstack / learnstack-dev-secret

# Web app
open http://localhost:3000 # one of the demo tenants
Expand All@@ -168,9 +167,10 @@ Edit `.env` to flip `DEPLOYMENT_MODE`:
| Value | What happens |
|-------|--------------|
| `Development` (default) | `InProcessEventBus` + `InMemoryCacheService` + env vars for secrets. Dapr sidecar is still present but not exercised. |
| `SaaS` | `DaprEventBus` (Kafka) + `DaprCacheService` (Redis) + `DaprSecretProvider` (Vault) + `HubEntitlementProvider` pointing at the local Hub. Requires the `learnstack-hub` repo's `make dev` to be running. |
| `SaaS` | `DaprEventBus` (Kafka) + `DaprCacheService` (Valkey) + `DaprSecretProvider` (Vault) + `HubEntitlementProvider` pointing at the local Hub. Requires the `learnstack-hub` repo's `make dev` to be running. |
| `Dedicated` | Same as `SaaS` for the composition; in practice the Hub is dedicated to one tenant. |
| `SelfHosted` | `SignedLicenseKeyEntitlementProvider` reads `.lic` from `./secrets/license.lic`; no Hub interaction. |
| `SelfHostedOnline` | `HubEntitlementProvider` against the LearnStack-hosted Hub (phone-home daily, 30-day cached-projection grace per ADR-0020). |
| `SelfHostedAirGapped` | `SignedLicenseKeyEntitlementProvider` reads `.lic` from `./secrets/license.lic`; no Hub interaction. |

After changing `.env`, restart the API:

Expand Down
10 changes: 5 additions & 5 deletions .claude/skills/run-tests-locally/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,7 +60,7 @@ docker info >/dev/null && echo "docker OK"
backend/tests/
LearnStack.Tests.Unit/ # No DB, no Docker. Pure unit tests.
LearnStack.Tests.Architecture/ # Reflection + Roslyn + migration-scan rules.
LearnStack.Tests.Integration/ # Testcontainers (Postgres, Redis, optional Dapr).
LearnStack.Tests.Integration/ # Testcontainers (Postgres, Valkey, optional Dapr).
LearnStack.Tests.EndToEnd/ # Real HTTP API + frontend smoke.

frontend/apps/web/ # Vitest + axe-core + Playwright (E2E).
Expand DownExpand Up@@ -96,14 +96,14 @@ Common failure messages and fixes:
| `Every_TenantOwned_Table_HasRls_With_AppTenantId` | Migration missing `ENABLE ROW LEVEL SECURITY` + the policy. |
| `Integration_Event_Handlers_Use_InboxGuard` | Handler skipped `IsAlreadyProcessedAsync`; see [add-integration-event](../add-integration-event/SKILL.md). |
| `Dapr_PubSub_TopicNames_FollowConvention` | Topic isn't `learnstack.{module}.{aggregate}`. |
| `Modules_Do_Not_Inject_Redis_Directly` | Use `ICacheService` not `IConnectionMultiplexer`. |
| `Modules_Do_Not_Inject_Valkey_Directly` | Use `ICacheService` not `IConnectionMultiplexer`. |
| `LearnStack_Modules_DoNotReference_Hub` | Hub URL or namespace referenced outside the dedicated adapter. |
| `No_Source_Folder_Named_Verticals` | A `Verticals/` folder exists; ADR-0018 forbids. |
| `Core_Modules_HaveNo_DomainSpecific_Names` | A `Cefr`, `English`, `Asana`, etc. name appears in core. |

### Step 5: Run integration tests

Testcontainers spin Postgres + Redis per class fixture; runtime depends on Docker
Testcontainers spin Postgres + Valkey per class fixture; runtime depends on Docker
performance.

```bash
Expand All@@ -123,7 +123,7 @@ Common failure shapes:
| `TenantContextMissingException` | Test forgot `using fixture.AsTenant(...)`. |
| Empty result where rows should exist | Wrong tenant context — RLS works as designed. |
| `relation "<table>" does not exist` | Migration didn't apply; check the module's `Persistence/Migrations`. |
| Docker container fails to start | Port collision (5432, 6379) — stop local Postgres / Redis. |
| Docker container fails to start | Port collision (5432, 6379) — stop local Postgres / Valkey. |
| Test hangs | A handler awaiting Dapr in the dev fallback path; check `IEventBus` registration. |

### Step 6: Run frontend tests
Expand DownExpand Up@@ -226,7 +226,7 @@ dotnet test --blame-hang --blame-hang-timeout 5min
change — the lockfile must match.
- **Skipped architecture test.** Forbidden. If a test is marked `[Skip]`, treat
it as a bug.
- **Port collisions.** Local Postgres / Redis on default ports collides with
- **Port collisions.** Local Postgres / Valkey on default ports collides with
Testcontainers. Stop them.
- **`--no-build` after a source change.** Drop the flag — the test would run
against stale binaries.
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/standards-check/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -92,7 +92,7 @@ Walk every item in [CLAUDE.md § Hard rules](../../../CLAUDE.md) and
- [ ] **Tenant + organization isolation defense-in-depth from day one** —
marker + filter + RLS + arch test wherever a tenant-owned entity lands.
- [ ] **Self-hosted infrastructure preferred** (Keycloak with two realms,
LiveKit OSS, MinIO, Meilisearch, Kafka, Vault).
LiveKit OSS, SeaweedFS, Meilisearch, Kafka, Vault).
- [ ] **No domain-specific code in any module.** Domain shape = tenant data
per ADR-0018.
- [ ] **Foundation building blocks are Day-1.** Dapr / APISIX / audit
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,9 +37,9 @@ logs/

# Local infrastructure data
.data/
minio-data/
seaweedfs-data/
postgres-data/
redis-data/
valkey-data/

# Local-only analyses (not shared via git)
docs/analysis/
Expand Down
20 changes: 11 additions & 9 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,17 @@ Self-Hosted — backed by the companion **`learnstack-hub`** repository

## What state this is in

**Phase 01 in progress.** The repository now has the .NET 10 solution
scaffold under `backend/` (core + 7 modules × 4 projects + 4 test
projects including the non-skippable `LearnStack.Tests.Architecture`),
**Phase 01 packets 1-6 shipped.** The repository now has the .NET 10
solution scaffold under `backend/` (core + 7 modules × 4 projects + 4
test projects including the non-skippable `LearnStack.Tests.Architecture`),
the `pnpm` frontend monorepo under `frontend/` (`apps/web` Next.js App
Router + `packages/{config,ui,sdk}`), and the core local-dev compose
stack at `infra/compose/dev.yml` (Postgres / Redis / MinIO / Mailpit /
Meilisearch). The remaining Phase-01 packets — Keycloak, LiveKit, Kafka,
Vault, Dapr, APISIX, `make` targets, `.env.example`, CI, `make seed` —
land incrementally; see
Router + `packages/{config,ui,sdk}`), and the full local-dev compose
stack at `infra/compose/dev.yml` — PostgreSQL 18, Valkey, SeaweedFS,
Mailpit, Meilisearch, Keycloak (two realms), LiveKit OSS + Coturn,
Kafka + kafka-ui, Vault, Dapr sidecar + placement, APISIX in file-
driven standalone mode. The remaining Phase-01 packets (7-8) — `make`
orchestrator, `.env.example`, pre-commit hook, `e2e.yml`, GitHub Actions
CI, `make seed` — land incrementally; see
[docs/roadmap/phase-01-repository-tooling.md](docs/roadmap/phase-01-repository-tooling.md).
Module-level code references in the docs (e.g.
`LearnStack.Modules.Education.Application`, `ILiveClassProvider`,
Expand DownExpand Up@@ -88,7 +90,7 @@ let the entry point pick it.
- **Standards changes cite an ADR.** A new standard rule or a change to an existing one is paired with an ADR when the rule is non-trivial.
- **Modular monolith with four cross-module mechanisms** ([ADR-0010](docs/decisions/0010-cross-module-communication.md)): application contract, intra-module domain event, integration event via outbox (dispatched through Dapr pub/sub per Amendment 1), read-model projection. No fifth.
- **Tenant + organization isolation is defense-in-depth from day one** ([ADR-0003 Amendment 1](docs/decisions/0003-tenant-isolation-defense-in-depth.md), [ADR-0017](docs/decisions/0017-tenant-organization-hierarchy.md)): tenant + organization context + EF query filters + PostgreSQL RLS + architecture tests.
- **Self-hosted infrastructure preferred** for Keycloak (auth, with two realms — `learnstack` + `learnstack-hub`), LiveKit OSS (live classroom), MinIO (object storage), Meilisearch (search), Kafka (pub/sub backend), Vault (secrets). See ADRs 0004, 0005, 0014.
- **Self-hosted infrastructure preferred** for Keycloak (auth, with two realms — `learnstack` + `learnstack-hub`), LiveKit OSS (live classroom), SeaweedFS (object storage), Meilisearch (search), Kafka (pub/sub backend), Vault (secrets). See ADRs 0004, 0005, 0014.
- **The core platform stays domain-generic.** Domain-specific shapes (CEFR levels, English placement-test scoring, kyu/dan ranks, yoga asana catalogs, …) live as **tenant customization data** ([ADR-0018](docs/decisions/0018-tenant-driven-customization-model.md)), never as code in any module. There is no `Verticals/` folder. ADR-0011 is superseded.
- **Foundation building blocks are Day-1, not Phase-11.** Dapr (`IEventBus`/`ICacheService`/`ISecretProvider`), APISIX gateway, audit infrastructure, organization scope, entitlement projection socket, host-to-tenant resolver, and architecture tests all ship in Phase 02a — not as later hardening.
- **Provider adapters everywhere.** Payments, auth, storage, search, live classroom, notifications, **event bus, cache, secrets, Hub HTTPS contract, entitlement source, host resolver** — all sit behind interfaces. No SaaS lock-in in `Domain` or `Application`. See [20-infrastructure-stack.md](docs/standards/20-infrastructure-stack.md).
Expand Down
26 changes: 15 additions & 11 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,25 +17,29 @@ Dedicated control plane, plan editor, custom-domain admin, and license-key issua

## Status

Phase 01 in progress. The repository now holds the .NET 10 solution scaffold
(7 modules × 4 projects + 4 test projects with `No_Source_Folder_Named_Verticals`
Phase 01 packets 1-6 shipped. The repository now holds the .NET 10 solution
scaffold (7 modules × 4 projects + 4 test projects with `No_Source_Folder_Named_Verticals`
architecture test), the `pnpm` frontend monorepo (`apps/web` + `packages/{config,ui,sdk}`),
and the core local-dev `docker-compose` stack (Postgres / Redis / MinIO / Mailpit /
Meilisearch). Keycloak, LiveKit, Kafka, Vault, Dapr, APISIX, `make` targets, and
CI land in subsequent Phase-01 packets — see [docs/roadmap/phase-01-repository-tooling.md](docs/roadmap/phase-01-repository-tooling.md).
and the full local-dev `docker-compose` stack — PostgreSQL 18, Valkey, SeaweedFS,
Mailpit, Meilisearch, Keycloak (two realms), LiveKit OSS + Coturn, Kafka + kafka-ui,
Vault, Dapr sidecar + placement, APISIX (file-driven standalone). The remaining
Phase-01 packets — `make` targets, `.env.example`, pre-commit, `e2e.yml`, GitHub
Actions CI, `make seed` — land in packets 7-8; see [docs/roadmap/phase-01-repository-tooling.md](docs/roadmap/phase-01-repository-tooling.md).

## Direction At A Glance

- **Backend:** .NET 10, ASP.NET Core, Entity Framework Core, MediatR.
- **Database:** PostgreSQL 16, with Row-Level Security from day one. Tenant + **Organization**
- **Database:** PostgreSQL 18, with Row-Level Security from day one. Tenant + **Organization**
defense in depth ([ADR-0003 Amendment 1](docs/decisions/0003-tenant-isolation-defense-in-depth.md),
[ADR-0017](docs/decisions/0017-tenant-organization-hierarchy.md)).
- **Cache / Pub-Sub / Secrets:** Redis 7, Kafka, HashiCorp Vault — all accessed via
**Dapr** building blocks (`IEventBus`, `ICacheService`, `ISecretProvider`) per
- **Cache / Pub-Sub / Secrets:** Valkey 8 (RESP-protocol Linux-Foundation BSD fork
per [ADR-0030](docs/decisions/0030-redis-compatible-store-valkey.md)), Kafka,
HashiCorp Vault — all accessed via **Dapr** building blocks (`IEventBus`,
`ICacheService`, `ISecretProvider`) per
[ADR-0014](docs/decisions/0014-adopt-dapr.md).
- **API Gateway:** **APISIX** in standalone YAML-reload mode per
- **API Gateway:** **APISIX** in file-driven standalone (`data_plane`) mode per
[ADR-0015](docs/decisions/0015-api-gateway-apisix.md).
- **Object storage:** MinIO locally, S3-compatible storage in production.
- **Object storage:** SeaweedFS locally, S3-compatible storage in production.
- **Search:** Meilisearch initially.
- **Frontend:** Next.js 15 (App Router), TypeScript, React. **One** application
(`apps/web`) with route segments for public, studio, and portal — multi-app split
Expand All@@ -57,7 +61,7 @@ CI land in subsequent Phase-01 packets — see [docs/roadmap/phase-01-repository
- **Live classroom:** In-app WebRTC; **self-hosted LiveKit OSS** is the default;
LiveKit Cloud available behind the same `ILiveClassProvider` interface. A custom
WebRTC SFU is explicitly out of scope.
- **Recording:** Supported via LiveKit Egress to S3/MinIO; tenant-configurable;
- **Recording:** Supported via LiveKit Egress to S3/SeaweedFS; tenant-configurable;
consent-aware; off by default.
- **Deployment:** Triple deployment model per
[ADR-0020](docs/decisions/0020-triple-deployment-hybrid-license.md) —
Expand Down
Loading