Conversation
Replaces the placeholders introduced by POM-180 with full content for the eight first-party adapters. Each page follows the same template: Install → Configuration → Usage → Gotchas → See also Configuration tables are sourced from each adapter's *Options.cs file (via XML doc), so they stay in sync with the actual code. Pages added: - aspnetcore.md — TenantValidationMiddleware + options table - postgresql.md — event store + connection pool tuning - redis.md — cache adapter + multi-tenant key warning - zitadel.md — OIDC identity, SA key vs PAT, redirect URI templates - stripe.md — billing port, webhook validation, PII handling - lemonsqueezy.md — billing alt + license key API - listmonk.md — newsletter, basic auth, PII handling - openrouter.md — LLM proxy, model prefixes, latency tail Local DocFX build: 0 errors. Remaining warnings are cosmetic — DocFX warns about ../api/*.html links at build time because it checks input content, but these files are generated as build output and resolve correctly at runtime.
There was a problem hiding this comment.
Pull request overview
Replaces “coming soon” placeholders with full DocFX how-to pages for the 8 first-party adapters, intended to guide installation, configuration, usage, and common pitfalls.
Changes:
- Added detailed adapter documentation pages under
docs/adapters/following a consistent structure. - Included configuration examples + options tables for each adapter.
- Added usage snippets and “gotchas” sections with cross-links to API reference/concepts/ADRs.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/adapters/aspnetcore.md | Adds install/config/usage/gotchas for ASP.NET Core integration (tenant middleware, etc.). |
| docs/adapters/postgresql.md | Adds PostgreSQL adapter how-to with options + event store usage example. |
| docs/adapters/redis.md | Adds Redis adapter how-to with options + usage/gotchas. |
| docs/adapters/zitadel.md | Adds Zitadel adapter how-to with auth/config options + provisioning usage snippet. |
| docs/adapters/stripe.md | Adds Stripe adapter how-to with options + billing/webhook guidance. |
| docs/adapters/lemonsqueezy.md | Adds LemonSqueezy adapter how-to with billing + license service usage. |
| docs/adapters/listmonk.md | Adds Listmonk adapter how-to with newsletter/email usage + security gotchas. |
| docs/adapters/openrouter.md | Adds OpenRouter adapter how-to with options + completion usage/gotchas. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| |---|---|---| | ||
| | `SecretKey` | _required_ | Stripe secret key (`sk_live_…` or `sk_test_…`) | | ||
| | `PublishableKey` | `null` | Frontend-safe key; surfaced for clients | | ||
| | `WebhookSigningSecret` | _strongly recommended_ | `whsec_…` — when empty, signature validation is skipped (dev only) | |
There was a problem hiding this comment.
The “Default” column should reflect the actual default value. StripeOptions.WebhookSigningSecret defaults to an empty string (""), not “strongly recommended”; the recommendation belongs in the description while the default remains _empty_/"" (since empty disables signature validation).
| | `WebhookSigningSecret` | _strongly recommended_ | `whsec_…` — when empty, signature validation is skipped (dev only) | | |
| | `WebhookSigningSecret` | `""` | Strongly recommended: set to `whsec_…`; when empty, signature validation is skipped (dev only) | |
| : ICommandHandler<StartSubscriptionCommand> | ||
| { | ||
| public async Task<Result> Handle(StartSubscriptionCommand cmd, CancellationToken ct) | ||
| { | ||
| var customerResult = await billing.EnsureCustomerAsync( | ||
| email: cmd.Email, | ||
| tenantId: cmd.TenantId, | ||
| ct); | ||
| if (customerResult.IsFailure) return customerResult.Error; | ||
|
|
||
| var subResult = await billing.StartSubscriptionAsync( | ||
| customerResult.Value.Id, cmd.PriceId, ct); | ||
| return subResult.IsSuccess ? Result.Success() : subResult.Error; |
There was a problem hiding this comment.
The usage snippet calls billing.EnsureCustomerAsync(...) and billing.StartSubscriptionAsync(...), but IBillingService doesn’t define those methods (it has UpsertCustomerAsync, CreateCheckoutSessionAsync, etc.). This example should be updated to use the real Compendium.Abstractions.Billing APIs (and ISubscriptionService if you want to show subscription operations).
| : ICommandHandler<StartSubscriptionCommand> | |
| { | |
| public async Task<Result> Handle(StartSubscriptionCommand cmd, CancellationToken ct) | |
| { | |
| var customerResult = await billing.EnsureCustomerAsync( | |
| email: cmd.Email, | |
| tenantId: cmd.TenantId, | |
| ct); | |
| if (customerResult.IsFailure) return customerResult.Error; | |
| var subResult = await billing.StartSubscriptionAsync( | |
| customerResult.Value.Id, cmd.PriceId, ct); | |
| return subResult.IsSuccess ? Result.Success() : subResult.Error; | |
| { | |
| public async Task<Result<string>> Handle(StartSubscriptionCommand cmd, CancellationToken ct) | |
| { | |
| var customerResult = await billing.UpsertCustomerAsync( | |
| email: cmd.Email, | |
| tenantId: cmd.TenantId, | |
| ct); | |
| if (customerResult.IsFailure) return customerResult.Error; | |
| var sessionResult = await billing.CreateCheckoutSessionAsync( | |
| customerId: customerResult.Value.Id, | |
| priceId: cmd.PriceId, | |
| successUrl: cmd.SuccessUrl, | |
| cancelUrl: cmd.CancelUrl, | |
| ct); | |
| return sessionResult.IsSuccess | |
| ? sessionResult.Value.Url | |
| : sessionResult.Error; |
| email: cmd.Email, | ||
| listId: cmd.ListId.ToString(), | ||
| attributes: new Dictionary<string, object> { ["source"] = "checkout" }, |
There was a problem hiding this comment.
INewsletterService.SubscribeAsync takes a SubscribeRequest object; there is no overload that accepts email, listId, and attributes as separate parameters. This example should construct new SubscribeRequest { Email = ..., ListIds = [...], Attributes = ... } and pass that into SubscribeAsync.
| email: cmd.Email, | |
| listId: cmd.ListId.ToString(), | |
| attributes: new Dictionary<string, object> { ["source"] = "checkout" }, | |
| new SubscribeRequest | |
| { | |
| Email = cmd.Email, | |
| ListIds = [cmd.ListId.ToString()], | |
| Attributes = new Dictionary<string, object> { ["source"] = "checkout" } | |
| }, |
| var result = await ai.CompleteAsync(new CompletionRequest | ||
| { | ||
| Model = "anthropic/claude-3.5-sonnet", | ||
| Messages = [new("user", q.Text)], |
There was a problem hiding this comment.
CompletionRequest.Messages is a list of Message (with Role: MessageRole + Content) and there’s no new("user", ...) constructor. The sample should use Message.User(q.Text) (or new Message { Role = MessageRole.User, Content = ... }) so it compiles against Compendium.Abstractions.AI.
| Messages = [new("user", q.Text)], | |
| Messages = [Message.User(q.Text)], |
| | `ServiceAccountKeyJson` | `null` | Service-account key JSON inline (use for K8s secrets) | | ||
| | `ServiceAccountKeyPath` | `null` | Path to service-account JSON (alternative to inline) | | ||
| | `ClientId` / `ClientSecret` | `null` | OAuth2 client credentials flow (alternative to SA key) | | ||
| | `PersonalAccessToken` | `null` | PAT used directly as Bearer (simplest auth path) | |
There was a problem hiding this comment.
ServiceAccountKeyJson / ServiceAccountKeyPath are documented as supported auth inputs here, but they are not currently used by the adapter when acquiring access tokens (only PersonalAccessToken or ClientId/ClientSecret are used). The options table should reflect the real supported auth mechanisms to avoid misconfiguration.
| public sealed class CreateTenantHandler(IIdentityProvider identity) | ||
| : ICommandHandler<CreateTenantCommand, TenantId> | ||
| { | ||
| public async Task<Result<TenantId>> Handle(CreateTenantCommand cmd, CancellationToken ct) | ||
| { | ||
| var orgResult = await identity.CreateOrganizationAsync(cmd.Name, ct); |
There was a problem hiding this comment.
The usage snippet uses IIdentityProvider and CreateOrganizationAsync(cmd.Name, ct), but there is no IIdentityProvider abstraction in Compendium.Abstractions.Identity. The org API is IOrganizationService.CreateOrganizationAsync(CreateOrganizationRequest request, ...), so this sample won’t compile as written.
| public sealed class CreateTenantHandler(IIdentityProvider identity) | |
| : ICommandHandler<CreateTenantCommand, TenantId> | |
| { | |
| public async Task<Result<TenantId>> Handle(CreateTenantCommand cmd, CancellationToken ct) | |
| { | |
| var orgResult = await identity.CreateOrganizationAsync(cmd.Name, ct); | |
| public sealed class CreateTenantHandler(IOrganizationService organizations) | |
| : ICommandHandler<CreateTenantCommand, TenantId> | |
| { | |
| public async Task<Result<TenantId>> Handle(CreateTenantCommand cmd, CancellationToken ct) | |
| { | |
| var request = new CreateOrganizationRequest(cmd.Name); | |
| var orgResult = await organizations.CreateOrganizationAsync(request, ct); |
| You need a Zitadel instance and either a service-account JSON key or a Personal Access Token (PAT). | ||
|
|
There was a problem hiding this comment.
This says a service-account JSON key can be used for auth, but the current adapter token acquisition logic only supports PersonalAccessToken or OAuth ClientId/ClientSecret (the service-account key options are never read). Please adjust this auth guidance to match the implemented behavior (or implement SA key support).
| public sealed class CachedOrderRepository(ICacheStore cache, IOrderRepository inner) | ||
| : IOrderRepository | ||
| { | ||
| public async Task<Order?> FindAsync(OrderId id, CancellationToken ct) | ||
| { | ||
| var key = $"order:{id}"; | ||
| var cached = await cache.GetAsync<Order>(key, ct); | ||
| if (cached is not null) return cached; | ||
|
|
||
| var order = await inner.FindAsync(id, ct); | ||
| if (order is not null) | ||
| await cache.SetAsync(key, order, TimeSpan.FromMinutes(5), ct); |
There was a problem hiding this comment.
The usage snippet references ICacheStore, GetAsync<T>, and SetAsync, but there is no ICacheStore interface in this repo and the Redis adapter doesn’t ship those APIs. The example should be rewritten to use the actual Redis integration points (e.g., IIdempotencyStore / IProjectionCheckpointStore / IConnectionMultiplexer).
| public sealed class CachedOrderRepository(ICacheStore cache, IOrderRepository inner) | |
| : IOrderRepository | |
| { | |
| public async Task<Order?> FindAsync(OrderId id, CancellationToken ct) | |
| { | |
| var key = $"order:{id}"; | |
| var cached = await cache.GetAsync<Order>(key, ct); | |
| if (cached is not null) return cached; | |
| var order = await inner.FindAsync(id, ct); | |
| if (order is not null) | |
| await cache.SetAsync(key, order, TimeSpan.FromMinutes(5), ct); | |
| using System.Text.Json; | |
| using StackExchange.Redis; | |
| public sealed class CachedOrderRepository(IConnectionMultiplexer redis, IOrderRepository inner) | |
| : IOrderRepository | |
| { | |
| public async Task<Order?> FindAsync(OrderId id, CancellationToken ct) | |
| { | |
| var db = redis.GetDatabase(); | |
| var key = $"order:{id}"; | |
| var cached = await db.StringGetAsync(key); | |
| if (cached.HasValue) | |
| return JsonSerializer.Deserialize<Order>(cached!); | |
| var order = await inner.FindAsync(id, ct); | |
| if (order is not null) | |
| { | |
| var payload = JsonSerializer.Serialize(order); | |
| await db.StringSetAsync(key, payload, TimeSpan.FromMinutes(5)); | |
| } |
| await eventStore.AppendAsync( | ||
| orderResult.Value.Id.ToString(), | ||
| orderResult.Value.DomainEvents, | ||
| expectedVersion: 0, | ||
| ct); |
There was a problem hiding this comment.
IEventStore does not have an AppendAsync method. The abstraction defines AppendEventsAsync(string aggregateId, IEnumerable<IDomainEvent> events, long expectedVersion, CancellationToken ct = default), so this sample won’t compile as written.
| } | ||
| ``` | ||
|
|
||
| For schema bootstrapping in a non-dev environment, run the migrations under `tests/Integration/.../Database/` as a reference, or write your own migrations. |
There was a problem hiding this comment.
This refers to migrations under tests/Integration/.../Database/, but there’s no such directory in the repo (tests use AutoCreateSchema / in-code CREATE TABLE IF NOT EXISTS instead). Please update the reference to point at a real schema bootstrap source (or remove it).
| For schema bootstrapping in a non-dev environment, run the migrations under `tests/Integration/.../Database/` as a reference, or write your own migrations. | |
| For schema bootstrapping in a non-dev environment, use the adapter's `AutoCreateSchema` / in-code `CREATE TABLE IF NOT EXISTS` behavior as a reference for the required tables, then create and manage your own version-controlled migrations. |
## Summary PR #23 incorrectly bumped CHANGELOG to `[1.0.0-preview.2] - 2026-04-26` with the quality-sweep entries — but tag `v1.0.0-preview.2` was already cut on **2026-04-25** from a different commit set (PRs #1-7) and **already published to nuget.org** (`Compendium.Core 1.0.0-preview.2` is live). Reusing that version was a mistake. This PR reconciles the CHANGELOG with the published reality and rolls today's work into a new **preview.3**: ### `[1.0.0-preview.2] - 2026-04-25` — rewritten retroactively Now matches the auto-generated GitHub release notes for `v1.0.0-preview.2`: - **Added** — `Compendium.Adapters.Shared` (PII masking utilities, introduced in #3). - **Changed** — Dependabot bumps #4-7, OSS governance scaffolding. - **Security** — workflow `permissions:` block (#1), tenant log sanitization (#2), email removal from adapter logs / GDPR (#3). ### `[1.0.0-preview.3] - 2026-04-26` — new Everything since `v1.0.0-preview.2`: - **Added** — DocFX site (#17), 5 ADRs (#14), public ROADMAP (#15), getting-started guide (#20), 4 concept pages (#21), 8 adapter how-to guides (#22). - **Changed** — CodeQL Default Setup → `extended` query suite. - **Security** — `softprops/action-gh-release` pinned to commit SHA (#16, alert #28 closed). ## Test plan - [ ] CI green on this PR. - [ ] After merge, tag `v1.0.0-preview.3` triggers Release workflow successfully. - [ ] `Compendium.* @ 1.0.0-preview.3` published on nuget.org. - [ ] GitHub Release `v1.0.0-preview.3` created with auto-generated notes. VK: POM-186 (Code Quality sweep parent). Co-authored-by: sacha <sacha@scojhconsult.com>
Summary
Closes POM-184. Replaces the placeholders from POM-180 with full how-to content for all 8 first-party adapters.
Pages
aspnetcore.mdpostgresql.mdredis.mdzitadel.mdstripe.mdlemonsqueezy.mdlistmonk.mdopenrouter.mdTemplate
Every page follows the same structure: Install → Configuration → Usage → Gotchas → See also. Configuration tables are derived from each adapter's
*Options.csXML doc — single source of truth.Validation
DocFX local build: 0 errors, 12 warnings.
The warnings are cosmetic: DocFX checks
../api/*.htmllinks against build input content, but those files are generated as build output. Verified manually that the corresponding.ymlfiles (input) and.htmlfiles (output) exist for every link, so the links resolve correctly on the deployed site.Test plan
docfx metadata + buildsucceeds locally*Options.csfiles/main/adapters/<name>.html