The control plane application for LearnStack — a separate codebase that owns tenant lifecycle, subscription / plan / billing, license issuance, entitlement projection, custom-domain administration, compliance caps, and the operator portal.
LearnStack Hub is not an LMS, not a tenant-facing surface, and never stores tenant content. Hub holds tenant metadata (plan, subscription, license, custom domain, compliance caps); tenant data (courses, lessons, learners, enrollments, classroom sessions) lives exclusively inside LearnStack core.
P02c-0 — Repository bootstrap ✅. Solution scaffold, frontend monorepo, compose stack, CI, and the docs skeleton are in place.
P02c-1 (Hub Domain Core) shipped 2026-08-09. It was reviewed against the restructured corpus first: although it was written before ADR-0033, ADR-0034 and ADR-0035, it implements none of what they changed — its entitlement wire shape already carries grace_until and generation, it hosts no endpoint, and its audit behavior is a shell.
The track is frozen from P02c-2 onward (owner decision, 2026-08-08), on the ADR-0035 trigger for IEntitlementProvider: a tenant must be billed or plan-gated. Until then LearnStack runs on NullEntitlementProvider and needs nothing from the Hub. See CLAUDE.md and docs/roadmap/p02c-1-hub-domain-core.md.
The Hub plan lives in this repository. docs/roadmap/ carries a document per packet (P02c-0 … P02c-7) plus the post-02c billing and marketplace tracks. LearnStack's phase-02c covers only LearnStack's side of the boundary.
make install # one-time: deps + git hooks
make dev # bring Hub-side compose stack up (requires the LearnStack compose already running)LearnStack Hub ships as a separate git repository per ADR-0019 — separate CI/CD, separate release cadence, separate Keycloak realm (learnstack-hub vs. learnstack), separate operator audit stream. The two repos communicate over an internal HTTPS surface carrying mTLS + signed JWT + HMAC body signature on every call.
The architecture deep-dive lives in the sibling repo: docs/architecture/24-learnstack-hub.md.
Hub documentation links to LearnStack core by absolute URL (https://github.com/HodeTech/LearnStack/blob/main/docs/...), per Documentation Standards § Layout. A relative path does not cross a repository boundary on github.com and depends on a sibling checkout being present and identically capitalised; an absolute URL works everywhere. Shell commands and filesystem references still use ../LearnStack — those are paths, not links — so the expected on-disk layout still matters:
<parent-dir>/
├── LearnStack/ (https://github.com/HodeTech/LearnStack)
└── LearnStack-Hub/ (this repo)
The capitalisation matters for the shell paths: a lower-cased spelling works on macOS's case-insensitive filesystem and fails on Linux CI. Documentation links are unaffected — they are absolute, and the CI link audit now checks every relative link it finds, because a relative cross-repo link is a defect rather than an exemption.
If you check out the two repos somewhere other than as siblings, the shell commands below need adjusting; the documentation links keep working. Multi-root workspace files (*.code-workspace) are gitignored — each developer keeps their own.
Hub repo's compose stack is deliberately minimal: it only runs services that are Hub-specific (Hub Dapr sidecar, Hub APISIX gateway, Postgres init). Everything else — Postgres, Valkey, Vault, Kafka, Keycloak, Mailpit — is shared with LearnStack core's compose stack and reached via host.docker.internal.
Boot order (each in its own terminal):
# 1. LearnStack core compose (shared backends)
cd ../LearnStack
make dev
# 2. Hub-side compose (Dapr sidecar + Hub APISIX + Postgres init for learnstack_hub DB)
cd ../LearnStack-Hub
make dev
# 3. LearnStack core API
# Both projects pin .NET 10 in global.json, and the system `dotnet` on this
# workstation is .NET 9 — so invoke the SDK explicitly. `export PATH="$HOME/.dotnet:$PATH"`
# once per shell works too; the explicit path is used here so a copy-pasted
# line works on its own.
cd ../LearnStack/backend
~/.dotnet/dotnet run --project src/LearnStack.Api
# 4. Hub API
# The host does not load .env itself, so export it first — otherwise the
# connection string resolves to defaults and the API cannot reach Postgres.
cd ../LearnStack-Hub
set -a; . ./.env; set +a
export POSTGRES_HOST=localhost
cd backend
~/.dotnet/dotnet run --project src/Core/LearnStack.Hub.ApiIn production, the two repos deploy independently — the shared compose is a dev-time convenience only.
- Backend: .NET 10 + ASP.NET Core + EF Core + MediatR + Hangfire (queue jobs land in P02c-5+).
- Database: PostgreSQL 18 (shared instance in dev under
learnstack_hubdatabase; separate instance in prod). Hub does not use Row-Level Security — Hub data is operator-administered, not tenant-isolated. - Cache / Pub-Sub / Secrets: Valkey 8 (shared instance,
hub:*namespace), Kafka (shared cluster,learnstack.hub.*topic prefix), Vault (shared instance,learnstack-hub/*path prefix) — all accessed via Hub's own Dapr sidecar. - API Gateway: APISIX in standalone YAML mode on its own port (9180 / 9543); separate instance from LearnStack core's APISIX.
- Frontend: Next.js 15.5 (App Router) operator portal under
frontend/apps/operator-portal; flat-config + Next 16 migration tracked in P02c-4 (apps/operator-portal/.eslintrc.cjsTODO). Authenticates against thelearnstack-hubKeycloak realm with MFA required. - Identity: Keycloak
learnstack-hubrealm (separate fromlearnstacktenant-facing realm). The realm export lives in the sibling repo at../LearnStack/infra/keycloak/realms/learnstack-hub.jsonbecause LearnStack core's compose imports both realms at first boot. - Architecture: Modular monolith (mirrors LearnStack's pattern) with explicit module contracts.
Governed by two invariants rather than by a count, per ADR-0034:
- The Hub stores no tenant content — enforced by
Hub_NeverStores_TenantData. - Every LearnStack↔Hub crossing goes through a named adapter —
IEntitlementProvider,IUsageReporter,IHubTenantSync. Nothing else holds a Hub client, and nothing resolves a host by calling the Hub.
Hub → LearnStack (/api/internal/*, internal listener only, hosted in LearnStack core):
| Method | Path | Purpose |
|---|---|---|
POST |
/api/internal/tenants |
Create tenant + default organization |
PUT |
/api/internal/tenants/{id}/entitlements |
Push the entitlement projection |
PUT |
/api/internal/tenants/{id}/status |
Suspend / activate / archive |
DELETE |
/api/internal/tenants/{id} |
Terminate |
GET |
/api/internal/tenants/{id}/usage |
Pull aggregated usage |
PUT |
/api/internal/tenants/{id}/host-mappings |
Push host → (tenant_id, organization_id?) mappings |
LearnStack → Hub (hosted here):
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/internal/license/verify |
Verify / pull the entitlement projection |
POST |
/api/v1/internal/license/refresh |
Scheduled phone-home refresh |
POST |
/api/v1/usage/report |
Report a usage metric (idempotent) |
Every call carries mTLS (LearnStack-internal CA) + RS256 JWT (aud=learnstack-internal, exp ≤ 5min) + HMAC-SHA256 body signature in X-Signature. Adding an endpoint requires a new ADR in ../LearnStack/docs/decisions/, landed in both repositories.
The Hub's own tenant-facing and operator-facing APIs (/api/v1/tenants/*, /api/v1/subscriptions/*, /api/v1/webhooks/*) are not part of this surface — they are the Hub's public API, governed here.
TLS certificates and private keys never travel in the entitlement payload. Host mappings go through PUT /api/internal/tenants/{id}/host-mappings; cert material moves between the two secret stores by replication and is referenced by path.
- docs/README.md — Hub doc catalogue.
- docs/roadmap/README.md — the Hub plan, owned here: one document per packet plus the cross-repo blocking table in both directions.
- docs/architecture/README.md — Hub architecture index.
- docs/architecture/contract-with-learnstack.md — Pointers to LearnStack-side authoritative contracts.
- docs/architecture/repository-layout.md — Hub internal folder structure + module topology.
- docs/decisions/ — Hub-internal ADRs (
HUB-NNNNseries). - docs/operations/ — Hub operational runbooks.
- docs/modules/ — Hub module deep dives.
- docs/glossary.md — Hub-specific terms.
- ADR-0019 LearnStack Hub
- ADR-0020 Triple Deployment + Hybrid License
- ADR-0021 Feature-Based Entitlement
- ADR-0022 Custom Domain + TLS (Amendment 1; its cert-delivery step is superseded by ADR-0034)
- ADR-0004 Authentication Strategy (Amendment 1 —
learnstack-hubrealm) - ADR-0034 Hub Contract Surface Invariant — the two invariants and the authoritative endpoint table
- ADR-0035 Demand-Gated Infrastructure — why the Hub track waits on a trigger rather than a date
- Architecture 24 LearnStack Hub
- Standards 20 Infrastructure Stack
- Phase 02c — LearnStack side
- All documentation in English (mirrors ADR-0007).
- Diagrams use Mermaid in fenced code blocks.
- Cross-cutting architectural decisions live in LearnStack core under
../LearnStack/docs/decisions/; Hub-internal-only decisions live here underdocs/decisions/with theHUB-NNNNnumbering series so they never collide with LearnStack ADR numbers. The Hub roadmap runs the other way — it is owned here, and LearnStack links to it. - Engineering rules from LearnStack's Standards corpus apply here unless explicitly overridden by a Hub-internal ADR.
- Single source of truth: each piece of knowledge lives in exactly one place. Linking is preferred over copying.