Cross-cutting infrastructure runtime per ADR-0038. Three building blocks are adopted; everything else is out of scope per ADR-0038 non-goals.
| Building block | Backend (dev) | Component file | Application interface |
|---|---|---|---|
| Pub/Sub | Kafka (kafka:9092) |
components/pubsub-kafka.yaml |
IEventBus |
| State store | Valkey (valkey:6379, RESP protocol) |
components/statestore-redis.yaml |
ICacheService |
| Secret store | Vault (http://vault:8200, dev mode) |
components/secretstore-vault.yaml |
ISecretProvider |
Phase ownership (per phase-02a § Shared Kernel and § Dapr Building Blocks, and phase-02b):
- Phase 02a declares all three interfaces in
LearnStack.SharedKernelwith default in-process implementations —InProcessEventBus,InMemoryCacheService, andConfigurationSecretProvider. All three exist today and are wired at the composition root. Those defaults are the only implementations registered, in everyDeploymentMode. - Phase 11 ships the Dapr-backed implementations (
DaprEventBus,DaprCacheService,DaprSecretProvider) inLearnStack.Infrastructure, on the triggers written in ADR-0035. Until then this compose stack runs Dapr, Kafka, Valkey and Vault for local familiarity only — no backend code path reaches them, andbackend/Directory.Packages.propsdeclares no client library for any of the four. - P02b-2 Step 3 implements the API-host
OutboxProcessor, with both independent review rounds complete. It is the only sanctioned production caller ofIEventBus.PublishAsyncper ADR-0010 Amendment 1. Modules consumeICacheServiceandISecretProviderfrom 02a and enqueue throughIOutbox; they never publish throughIEventBusdirectly.
Service invocation, workflow, bindings, actors, configuration, and
distributed lock are not adopted; if a future need appears the gate is a
new ADR. The state store's actorStateStore flag is therefore pinned to
"false" — see the comment at the top of components/statestore-redis.yaml.
Dev compose runs one sidecar bound to the learnstack-api app id:
graph LR
app["<b>.NET API</b><br/>dotnet run → host:5080<br/>(workstation, outside compose network)"]
daprd["<b>daprd</b> (compose service: dapr-sidecar-api)<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"]
daprd -- "inbound subscription delivery<br/>via host.docker.internal:5080" --> app
app -- "outbound publish / get-secret / get-state<br/>via localhost:3500 (HTTP) / :50001 (gRPC)" --> daprd
Text fallback for non-Mermaid renderers:
- App —
dotnet runon the developer's workstation, listening onhost:5080(outside the compose network). - daprd — compose service
dapr-sidecar-api, run with flags:-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. - daprd → app — inbound subscription deliveries reach the workstation
via
host.docker.internal:5080. - app → daprd — outbound publish / state / secret calls hit
localhost:3500(HTTP) orlocalhost:50001(gRPC).
The .NET host runs outside the container network during active dev
(developers dotnet run from their workstation). -app-channel-address host.docker.internal is what makes inbound subscription deliveries reach
the host — daprd's default of 127.0.0.1 would resolve to inside the
sidecar container, and every subscription would silently fail to deliver.
The extra_hosts: host.docker.internal:host-gateway YAML anchor in
infra/compose/dev.yml maps the alias on Linux (Docker Desktop does it
automatically).
The placement service (dapr-placement) is required even though actors are
out of scope — daprd won't start without it.
Per ADR-0038 + Standards 20, modules never import Dapr.Client. They
consume:
public interface IEventBus { Task PublishAsync(IntegrationEventEnvelope envelope, CancellationToken ct); }
public interface ICacheService { Task<T?> GetAsync<T>(string key, CancellationToken ct); /* … */ }
public interface ISecretProvider { Task<string> GetSecretAsync(string key, CancellationToken ct); /* … */ }DaprEventBus, DaprCacheService, DaprSecretProvider are the only
Dapr-aware types in the codebase; they live in LearnStack.Infrastructure
and ship in Phase 11 on ADR-0035's triggers.
Architecture tests
Dapr_SDK_Types_NotImportedOutsideInfrastructure,
Modules_DoNotReference_DaprPackage, and
ICacheService_Is_OnlyCacheAbstraction keep this honest.
| Surface | Credential |
|---|---|
| Vault root token | learnstack-dev-root-token |
| Kafka auth | none (authType: none, disableTls: true) |
| Valkey password | (empty) |
All dev-only. Production wires Vault with AppRole / Kubernetes auth and
loads the token through Dapr's secretKeyRef indirection so the literal
token never appears in the component YAML.
As of Phase 01 packet 7 (DX), the Vault root token lives in one place:
VAULT_ROOT_TOKEN in the repo-root .env.example (copied to .env per
workstation). The chain uses Dapr's secretKeyRef + local-env-secret-store
indirection (a {{env.VAR}} template would be silently substituted with the
literal — Dapr does not support that syntax in component metadata):
.env— developer's actual value (gitignored).infra/compose/dev.yml— both thevaultservice (boots-devmode with that token) and thedapr-sidecar-apiservice (passes it to daprd's process env) read${VAULT_ROOT_TOKEN:-learnstack-dev-root-token}.infra/dapr/components/secretstore-envvar.yamlregisters asecretstores.local.envcomponent namedenvvar-secrets(loaded first by daprd because no other component depends on it).infra/dapr/components/secretstore-vault.yamldeclaresauth.secretStore: envvar-secretsand avaultTokenmetadata entry resolved viasecretKeyRef: { name: VAULT_ROOT_TOKEN, key: VAULT_ROOT_TOKEN }. Dapr pulls the literal from the process env at component-load time.
Changing .env therefore updates every consumer at the next docker compose up. There is no two-file edit risk and no literal token in any committed
YAML metadata field.
- The
IEventBus/ICacheService/ISecretProviderimplementations — the interfaces and default adapters shipped in Phase 02a; Dapr-backed adapters belong to Phase 11 (see § Phase ownership above). - Outbox dispatcher (
OutboxProcessorpolling + dispatch) — Phase 02b. - Shared transport-owned
inbox_messagestable +IInboxGuard— P02b-3. - Production Vault setup (HA mode, auto-unseal, AppRole policies) — Phase 11.
- Additional sidecars for the Hub overlay (separate app id, separate consumer group) — Phase 02c.