LearnStack Hub is the control plane for LearnStack: tenant lifecycle, plan catalogue, subscriptions, the entitlement projection, custom-domain administration, licence issuance, and the operator portal. It holds tenant metadata and never tenant content.
This directory is the authoritative plan for Hub work. LearnStack's Phase 02c covers only LearnStack's side of the boundary — the HubEntitlementProvider / IUsageReporter / IHubTenantSync adapters and the /api/internal/* handlers — and points here for everything Hub-side. Cross-cutting decisions (ADRs, engineering standards) remain LearnStack's; see Where cross-cutting authority lives.
- Hub is a demand-gated track, not a prerequisite. LearnStack runs on
NullEntitlementProviderand resolves hosts fromplatform_host_to_tenantuntil a tenant must be billed or plan-gated. That sentence is the literal trigger condition ADR-0035 records for the Hub entitlement adapter. It is the first of two conditions that resume this plan; the second is that the contract surface is built against ADR-0034's two invariants rather than an endpoint count. Both are stated in CLAUDE.md and in The freeze. - The
P02c-Nidentifiers are stable and load-bearing. They appear in branch names, commit subjects, skill bodies, design specs, CI comments, and in LearnStack's roadmap. They are not renumbered, andP02c-3in this repo means the same packet asP02c-3in LearnStack's. - Every packet doc carries the same six sections —
## Goal,## Scope,## Deliverables,## Completion Criteria,## Risks,## Phase Exit Decision— matching LearnStack's roadmap convention. - No owners, no effort estimates, no timeboxes. This is a dependency and scope plan. Sequencing decisions belong here; capacity decisions do not.
| Packet | Title | State |
|---|---|---|
| P02c-0 | Repository bootstrap | ✅ Shipped |
| P02c-1 | Hub domain core + Hub cross-cutting foundation | ✅ Shipped — reconciliations owed |
| P02c-2 | Internal API handlers + outbound LearnStackApiClient |
⏸ Frozen — ADR-0035 trigger |
| P02c-3 | LearnStack integration (cross-repo, two coordinated PRs) | ⏸ Frozen — also blocked |
| P02c-4 | Operator portal MVP + Operators + Audit modules | ⏸ Frozen |
| P02c-5 | Custom-domain lifecycle + Compliance module | ⏸ Frozen |
| P02c-6 | Licence key (functional skeleton) | ⏸ Frozen |
| P02c-7 | End-to-end exit gate | ⏸ Frozen |
P02c-1 is merged. Everything after it is frozen by 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 consumes nothing from the Hub, so building
P02c-2's contract surface would be building against a boundary with no caller.
P02c-1 merged rather than waiting because its domain code conflicts with none of the three
decisions that moved: the entitlement wire shape already carries grace_until and
generation, no endpoint is hosted, and AuditLogBehavior is a shell that writes nothing.
Freezing the artefact would have cost the SharedKernel reconciliation against LearnStack
Packet 3b, which grows with every packet built on top.
Owned by p02c-1-hub-domain-core.md § Phase Exit Decision; repeated here because they outlive the packet and a reader of this index should not have to open it to learn they exist:
- Audit seam (ADR-0033).
AuditLogBehavioris step 3 and wrapsTransactionBehavior; its success-path TODO writes after commit. ADR-0033 puts the MUST-class write on the ambient transaction immediately beforeCOMMIT. Nothing misbehaves today — the behavior writes nothing — but the TODO specifies the superseded shape. Lands with the Audit module in P02c-4. - SharedKernel vs LearnStack Packet 3b. The mirrored kernel inherited all three
defects Packet 3b exists to repair:
Results.Unitcolliding withMediatR.Unit, missing[MemberNotNullWhen]onResult<T>, andEntity<TId>boxing on every equality check. Unlike LearnStack, the Hub already has four modules of consumers, so this grows with each packet.
- ⏳ Roslyn
DomainExceptionanalyzer (LearnStack.Hub.Analyzers) — deferred per cross-cutting-foundation.md § 5; theDomainException-vs-Result.Failrule rides code review until then. Target: P02c-2. - ⏳ EF-Core OpenTelemetry instrumentation (
AddEntityFrameworkCoreInstrumentation) — the only published package is a 1.x-beta whoseOpenTelemetry.Apifloor conflicts with the stable 1.15.x instrumentation set; reserved inDirectory.Packages.props. Target: P02c-2. - ⏳ Feature/limit registry sync — Hub seeds
FeatureKeys/LimitKeysfrom the projection wire-shape (Architecture 24 § 4); a cross-repo reconciliation with LearnStack core's registry (or a sharedLearnStack.Contractspackage) is the durable fix. Target: Phase 11. - ⏳ SQL keyset pagination — list repositories slice in memory in P02c-1 (tiny volume); promote to
ORDER BY ... WHERE id > cursorwhen volume warrants.
Execution artifacts live alongside the packet docs: P02c-1-implementation-prompt.md is the kickoff prompt the P02c-1 agent ran against.
These are Hub-owned phases that sit outside the P02c series. LearnStack's Phase 09b and Phase 12 are pointers at them.
| Track | What it covers | Trigger |
|---|---|---|
| Hub Billing | Stripe / Iyzico adapters, Invoicing module, WebhookLedger, dunning, proration, the storefront flow |
Commercial billing is needed |
| Hub Marketplace | Shared customization packages, listing, install — free-only; paid listings and revenue splits are out of the roadmap | Demonstrated cross-tenant duplication and an accepted ADR resolving the ADR-0034 collision |
| Hub Operations (unscheduled) | Hub production deployment, HA topology, backup and restore drill, release process, and the operational runbooks the P02c-7 gate names | A Hub instance serves a paying tenant |
Hub Operations has no plan document yet. It is listed so the work has a named owner in this repository rather than being handed to LearnStack Phase 11, which scopes itself to LearnStack (Phase 11 § Release Engineering).
flowchart TB
p0[P02c-0<br/>Repository bootstrap]
p1[P02c-1<br/>Domain core + foundation]
p2[P02c-2<br/>Internal API + client]
p3[P02c-3<br/>LearnStack integration]
p4[P02c-4<br/>Operator portal]
p5[P02c-5<br/>Custom domains + compliance]
p6[P02c-6<br/>Licence key]
p7[P02c-7<br/>Exit gate]
p0 --> p1
p1 -- "aggregates + projection serialiser" --> p2
p1 -- "AuditLog + Authorization shells promoted" --> p4
p1 -- "projection fields to extend" --> p5
p1 -- "expires_at / grace_until owner" --> p6
p2 -- "contract shape the LearnStack PR mirrors" --> p3
p2 -- "OpenAPI + generated SDK" --> p4
p2 -- "outbound client for host-mappings push" --> p5
p2 -- "verify endpoint the .lic path calls" --> p6
p4 -- "portal shell + Operators + Audit modules" --> p5
p4 -- "portal shell + operator audit" --> p6
p3 --> p7
p4 --> p7
p5 --> p7
p6 --> p7
Reading the map, for renderers without Mermaid:
- P02c-0 → P02c-1. The bootstrap ships empty projects; P02c-1 fills them.
- P02c-1 → P02c-2. The internal API serves the
Entitlementprojection and thePlan/HubSubscriptionaggregates P02c-1 creates. P02c-2 also promotes P02c-1'sOutboxFlushBehaviorshell to a liveIOutboxflush — until then there is nothing to publish. - P02c-1 → P02c-4. The operator portal reads the four aggregates, and P02c-4 promotes the other two P02c-1 pipeline shells:
AuditLogBehaviorgains the operator-audit writer andAuthorizationBehaviorgains the operator permission check. Both are registration-order-only in P02c-1. - P02c-1 → P02c-5 / P02c-6.
CompliancePolicyfills the projection'scompliance_caps, which P02c-1 ships as an empty JSONB shape; licence issuance fillsexpires_at/grace_until, which P02c-1 leaves null. - P02c-2 → P02c-3. The Hub PR carries the canonical contract shape; the LearnStack PR is written against it.
- P02c-2 → P02c-4. The portal consumes the generated SDK produced from P02c-2's OpenAPI document.
- P02c-2 → P02c-5. The host-mapping push uses the outbound
LearnStackApiClient. P02c-2 builds the client with no caller for that path; P02c-5 is the caller. - P02c-3 … P02c-6 → P02c-7. The exit gate rehearses the full SaaS and Self-Hosted scenarios end to end, so it gates on every preceding packet.
Both repositories block each other in places. Neither table is a wish list — every row names the artefact that unblocks it.
| Hub packet | LearnStack packet | What it provides |
|---|---|---|
| P02c-3 | P02a-5 Foundation ports | IEventBus / ICacheService / ISecretProvider and their default implementations, which the LearnStack-side handlers use |
| P02c-3 | P02a-6 Tenancy schema | platform_entitlement_cache, platform_host_to_tenant and outbox_messages tables |
| P02c-3 | P02a-7 Resolution + isolation | IHostToTenantResolver, TenantResolverMiddleware, and the HubCorrelationMiddleware seam that populates ITenantContext on /api/internal/* |
| P02c-3 | P02a-9 Audit + entitlement socket | The IEntitlementProvider socket with NullEntitlementProvider as its only implementation |
| P02c-3 | Phase 02b Identity Integration + Events | The OutboxProcessor and its claim protocol, IInboxGuard and the per-module inbox_messages tables, and handler-scope tenant-context restoration. IUsageReporter dispatches through the outbox rather than inline, and the learnstack.hub.entitlement invalidation consumer is an ordinary IIntegrationEventHandler<T> behind the same inbox guard |
| P02c-4 | LearnStack infra/keycloak/realms/learnstack-hub.json |
The operator roles and the MFA-required browser flow, added to the single owning realm export — LearnStack's compose imports both realms at first boot, so the file cannot be duplicated here. A coordinated pull request; the rest of P02c-4 is Hub-only |
| P02c-6 | P02a-9 Audit + entitlement socket | The IEntitlementProvider socket the LearnStack-side SignedLicenseKeyEntitlementProvider skeleton plugs into, in a coordinated pull request |
| P02c-5 | LearnStack Phase 02c | The LearnStack-side host-mappings handler and its platform_host_to_tenant mirroring — the paired half of this packet, merged in the same session |
| P02c-5 | LearnStack Phase 11 | The LearnStack edge half only: certificate installation at the gateway, demand-gated per ADR-0035. P02c-5 does not wait on it — host resolution works from the platform_host_to_tenant row alone |
P02c-0 and P02c-1 have shipped. Of what remains, only P02c-2 is fully Hub-local — it touches no LearnStack code and can proceed as soon as the Hub track resumes. P02c-3, P02c-4, P02c-5 and P02c-6 each land as two coordinated pull requests; see the tables above and Coordination protocol. P02c-4's cross-repo half is the smallest of the four — the operator roles and MFA browser flow in the realm export — but it is not zero, and calling the packet LearnStack-free is what let that edit go unplanned. P02c-3 is the only packet gated on the LearnStack spine reaching Phase 02b, not merely Phase 02a.
| LearnStack artefact | Hub packet | Why |
|---|---|---|
entitlement-v1.schema.json + its snapshot test |
P02c-1 | The schema is born with the Hub projection serialiser. Both repositories check in the same file and assert it independently. |
HubEntitlementProvider (Phase 02c) |
P02c-2 | The last hop of the entitlement read path in ADR-0034 is POST /api/v1/internal/license/verify, which is a Hub handler. |
IUsageReporter (Phase 02c) |
P02c-2 | Needs POST /api/v1/usage/report live, with its idempotency semantics fixed Hub-side. |
LearnStack /api/internal/* handlers (Phase 02c) |
P02c-2 | The Hub PR carries the canonical request / response shapes and the mTLS + JWT + HMAC chain the handlers validate against. |
NullEntitlementProvider_NotRegistered_OutsideDevelopment |
P02c-3 | The rule is vacuous until a second IEntitlementProvider implementation exists. |
platform_host_to_tenant mirror handler (Phase 02c) |
P02c-5 | The Hub is the certificate issuer and authors the PUT /api/internal/tenants/{id}/host-mappings payload shape. |
SignedLicenseKeyEntitlementProvider skeleton |
P02c-6 (coordinated LearnStack PR) | Needs the .lic format, the claim set, and the kid-addressed public key set the Self-Hosted instance ships with. Hardened later in LearnStack Phase 11. |
A packet that changes both repositories lands as two pull requests in one session: the Hub PR opens first and carries the canonical contract shape, the LearnStack PR references the Hub PR's commit hash, and both merge together. A one-sided merge leaves the contract dangling. P02c-3 is the packet where this matters most and states the protocol in full.
Updated 2026-08-09. This ledger tracks the state of artefacts, not commits. A row changes when the artefact changes state, not when a commit touches it — the previous commit-bound version of this file went stale four commits after it was written.
| Artefact | State | Where |
|---|---|---|
| Repository bootstrap | Shipped on main |
p02c-0-repository-bootstrap.md |
| Hub agent skill catalogue (18 skills) | Shipped on main |
.claude/skills/README.md |
| Hub architecture design specs (3) | Shipped on main |
module-topology, cross-cutting-foundation, entitlement-projection |
| Hub module design specs (4) | Shipped on main |
tenant-lifecycle, plans, subscriptions, entitlements |
| P02c-1 implementation prompt | Shipped on main |
P02c-1-implementation-prompt.md |
| P02c-1 implementation | Shipped on main |
Merged 2026-08-09, ~222 files. Two reconciliations owed on the merged code — the SharedKernel against LearnStack Packet 3b, and the audit seam against ADR-0033 — both listed in p02c-1-hub-domain-core.md. |
| Hub roadmap (this directory) | Authoritative | This file plus the eight packet docs |
| P02c-2 and everything after it | Frozen, not started | Owner decision 2026-08-08. Resumes on the ADR-0035 trigger — see The freeze. |
Known reconciliations still open, each named in its owning packet doc so it cannot be lost:
TheClosed 2026-08-09. P02c-1 shipped the first Testcontainers-backed tests and activated the job with them, which is the correct assignment; the three files onbackend-integrationCI job's owning packet.mainthat said P02c-2 were written when P02c-1 was expected to ship without integration tests, and are corrected.- Operator-portal naming: closed 2026-08-08. LearnStack's
docs/corpus now saysoperator-portaleverywhere. Three residues remain outside it and are tracked rather than open:frontend/README.mdandfrontend/apps/web/README.mdstill saylearnstack-hub-web, and ADR-0015 § Frontend apps saysapps/hub-web— a third spelling, corrected by the next PR that touches that ADR. LearnStack's frozen Phase 01 record keeps the old name deliberately, annotated in place. The Keycloak client idlearnstack-hub-webis an OIDC identifier, not an app name, and does not change. - Feature-key / limit-key registry drift between the two repositories, which each keep their own copy. Recorded in plans.md § Registry sync.
Hub does not maintain its own standards corpus and does not restate LearnStack's decisions. Each row below is a single source of truth; if anything in this repository contradicts one, this repository is what gets fixed.
| Topic | Authority |
|---|---|
| The Hub boundary, separate repository, internal API | ADR-0019 |
| Contract-surface invariants + the real endpoint set | ADR-0034 |
| What ships now versus on demand, and the triggers | ADR-0035 |
| Deployment modes + hybrid licence | ADR-0020 |
| Entitlement projection shape | ADR-0021 |
| Custom domain + TLS | ADR-0022 |
| Two-realm Keycloak boundary | ADR-0004 |
| Exception handling, logging, observability | ADR-0032 |
| Engineering standards | Standards corpus |
| Architecture-test identifiers | Standards 21 |
| Hub-internal decisions only | docs/decisions/ — the HUB-NNNN series |
One correction worth stating explicitly. ADR-0034 replaces the "the Hub contract surface is closed at four endpoints" rule with two invariants: the Hub stores no tenant content, and every LearnStack↔Hub crossing goes through a named adapter. Adding an endpoint still requires an ADR. Pointer text elsewhere in this repository that still says "four endpoints" predates that ADR and is corrected as those files are touched.