Skip to content

docs: how-to guides for 8 adapters#22

Merged
Pomdapis merged 1 commit intomainfrom
pom-184-adapter-howtos
Apr 26, 2026
Merged

docs: how-to guides for 8 adapters#22
Pomdapis merged 1 commit intomainfrom
pom-184-adapter-howtos

Conversation

@Pomdapis
Copy link
Copy Markdown
Contributor

Summary

Closes POM-184. Replaces the placeholders from POM-180 with full how-to content for all 8 first-party adapters.

Pages

Adapter Length Highlights
aspnetcore.md ~50 lines TenantValidationMiddleware options, ordering with auth/authz
postgresql.md ~80 lines Connection pool tuning table, PgBouncer caveat
redis.md ~60 lines Multi-tenant key namespacing, TLS warning
zitadel.md ~75 lines SA key vs PAT vs client-credentials, redirect URI templates
stripe.md ~65 lines Webhook signature validation, API version pinning, PII handling
lemonsqueezy.md ~55 lines License key API specifics, JSON:API vs plain JSON
listmonk.md ~55 lines PII in logs, basic auth over HTTPS only
openrouter.md ~70 lines Model prefixing, latency tail, prompt logging hazard

Template

Every page follows the same structure: Install → Configuration → Usage → Gotchas → See also. Configuration tables are derived from each adapter's *Options.cs XML doc — single source of truth.

Validation

DocFX local build: 0 errors, 12 warnings.

The warnings are cosmetic: DocFX checks ../api/*.html links against build input content, but those files are generated as build output. Verified manually that the corresponding .yml files (input) and .html files (output) exist for every link, so the links resolve correctly on the deployed site.

Test plan

  • docfx metadata + build succeeds locally
  • Configuration tables match the actual *Options.cs files
  • Cross-links to concept pages and ADRs are valid
  • CI green
  • Pages live at /main/adapters/<name>.html

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.
Copilot AI review requested due to automatic review settings April 25, 2026 10:44
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/adapters/stripe.md
|---|---|---|
| `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) |
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
| `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) |

Copilot uses AI. Check for mistakes.
Comment thread docs/adapters/stripe.md
Comment on lines +40 to +52
: 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;
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
: 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;

Copilot uses AI. Check for mistakes.
Comment thread docs/adapters/listmonk.md
Comment on lines +51 to +53
email: cmd.Email,
listId: cmd.ListId.ToString(),
attributes: new Dictionary<string, object> { ["source"] = "checkout" },
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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" }
},

Copilot uses AI. Check for mistakes.
var result = await ai.CompleteAsync(new CompletionRequest
{
Model = "anthropic/claude-3.5-sonnet",
Messages = [new("user", q.Text)],
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
Messages = [new("user", q.Text)],
Messages = [Message.User(q.Text)],

Copilot uses AI. Check for mistakes.
Comment thread docs/adapters/zitadel.md
Comment on lines +37 to +40
| `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) |
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread docs/adapters/zitadel.md
Comment on lines +55 to +60
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);
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment thread docs/adapters/zitadel.md
Comment on lines +11 to +12
You need a Zitadel instance and either a service-account JSON key or a Personal Access Token (PAT).

Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread docs/adapters/redis.md
Comment on lines +48 to +59
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);
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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));
}

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +67
await eventStore.AppendAsync(
orderResult.Value.Id.ToString(),
orderResult.Value.DomainEvents,
expectedVersion: 0,
ct);
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
}
```

For schema bootstrapping in a non-dev environment, run the migrations under `tests/Integration/.../Database/` as a reference, or write your own migrations.
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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.

Copilot uses AI. Check for mistakes.
@Pomdapis Pomdapis merged commit b1cddf1 into main Apr 26, 2026
9 checks passed
@Pomdapis Pomdapis deleted the pom-184-adapter-howtos branch April 26, 2026 11:34
Pomdapis added a commit that referenced this pull request Apr 26, 2026
## 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>
@Pomdapis Pomdapis mentioned this pull request Apr 26, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants