-
Notifications
You must be signed in to change notification settings - Fork 0
docs: how-to guides for 8 adapters #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,66 @@ | ||
| # Compendium.Adapters.AspNetCore | ||
|
|
||
| > Coming soon — tracked in [POM-184](https://github.com/sassy-solutions/compendium/issues?q=is%3Aissue+POM-184). | ||
| ASP.NET Core integration: tenant validation middleware, security headers, problem-details mapping, and authentication helpers. | ||
|
|
||
| ASP.NET Core integration: tenant validation middleware, problem details, auth helpers. | ||
| ## Install | ||
|
|
||
| ```bash | ||
| dotnet add package Compendium.Adapters.AspNetCore | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| The adapter exposes several middleware and helper registrations. Most projects wire the tenant middleware first. | ||
|
|
||
| ```csharp | ||
| using Compendium.Adapters.AspNetCore.Security; | ||
|
|
||
| var builder = WebApplication.CreateBuilder(args); | ||
|
|
||
| builder.Services.Configure<TenantValidationMiddlewareOptions>( | ||
| builder.Configuration.GetSection("Compendium:Tenant")); | ||
|
|
||
| var app = builder.Build(); | ||
|
|
||
| app.UseMiddleware<TenantValidationMiddleware>(); | ||
| // ... | ||
| app.Run(); | ||
| ``` | ||
|
|
||
| `TenantValidationMiddlewareOptions` (see [`TenantValidationMiddleware.cs`](https://github.com/sassy-solutions/compendium/blob/main/src/Adapters/Compendium.Adapters.AspNetCore/Security/TenantValidationMiddleware.cs)): | ||
|
|
||
| | Option | Default | Description | | ||
| |---|---|---| | ||
| | `TenantHeaderName` | `X-Tenant-ID` | HTTP header to read the tenant from | | ||
| | `EnableSubdomainResolution` | `true` | Whether to fall back to the host's subdomain | | ||
| | `IgnoredSubdomains` | `www, api, admin, app, dashboard, console, portal, staging, dev, test` | Subdomains *not* treated as tenant identifiers | | ||
| | `TenantClaimTypes` | `tenant_id, tid, org_id, organization_id, urn:zitadel:iam:org:id` | JWT claim names checked, in order | | ||
| | `ExcludedPaths` | `/health, /healthz, /ready, /live, /metrics, /.well-known, /swagger, /api-docs` | Paths skipped by tenant validation | | ||
|
|
||
| ## Usage | ||
|
|
||
| The middleware extracts the tenant from header / subdomain / JWT, validates that all sources agree, looks up the tenant via `ITenantStore`, and sets `TenantContext` for the rest of the pipeline. See [the multi-tenancy concept page](../concepts/multi-tenancy.md) for the full data flow. | ||
|
|
||
| Downstream handlers receive the tenant via DI: | ||
|
|
||
| ```csharp | ||
| public sealed class GetOrdersHandler(TenantContext tenant, IOrderRepository repo) | ||
| : IQueryHandler<GetOrdersQuery, IReadOnlyList<Order>> | ||
| { | ||
| public Task<IReadOnlyList<Order>> Handle(GetOrdersQuery q, CancellationToken ct) | ||
| => repo.ListAsync(tenant.Current.Id, ct); | ||
| } | ||
| ``` | ||
|
|
||
| ## Gotchas | ||
|
|
||
| - **Order matters.** Place the tenant middleware *after* authentication (so the JWT claim is available) but *before* authorization (so policies can read `TenantContext`). | ||
| - **`ExcludedPaths` are prefix matches.** `/health` matches `/health` and `/healthz` and `/health-check`. Pick paths intentionally. | ||
| - **CRLF in `Path`.** The middleware sanitizes user-controlled paths before logging (POM-175). If you mirror the pattern in your own middleware, do the same. | ||
| - **Subdomain resolution requires at least 3 host parts.** `acme.example.com` works; `localhost`, IPs, and bare-domain (`example.com`) don't. | ||
|
|
||
| ## See also | ||
|
|
||
| - [API Reference: Compendium.Adapters.AspNetCore.Security](../api/Compendium.Adapters.AspNetCore.Security.html) | ||
| - [Multi-tenancy concept](../concepts/multi-tenancy.md) | ||
| - [ADR 0004 — Multi-tenancy strategy](../adr/0004-multi-tenancy-strategy.md) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,67 @@ | ||
| # Compendium.Adapters.LemonSqueezy | ||
|
|
||
| > Coming soon — tracked in [POM-184](https://github.com/sassy-solutions/compendium/issues?q=is%3Aissue+POM-184). | ||
| [LemonSqueezy](https://www.lemonsqueezy.com/) billing adapter — alternative to Stripe, focused on merchant-of-record billing and license-key delivery for software products. Implements the same `Compendium.Abstractions.Billing` port as the Stripe adapter, so swapping between them is a DI change. | ||
|
|
||
| LemonSqueezy billing adapter. | ||
| ## Install | ||
|
|
||
| ```bash | ||
| dotnet add package Compendium.Adapters.LemonSqueezy | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| ```json | ||
| { | ||
| "LemonSqueezy": { | ||
| "ApiKey": "eyJ0eXAi...", | ||
| "StoreId": "12345", | ||
| "WebhookSigningSecret": "your-webhook-secret", | ||
| "BaseUrl": "https://api.lemonsqueezy.com/v1/", | ||
| "TimeoutSeconds": 30 | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Options (`LemonSqueezyOptions`): | ||
|
|
||
| | Option | Default | Description | | ||
| |---|---|---| | ||
| | `ApiKey` | _required_ | LemonSqueezy API key (Bearer token) | | ||
| | `StoreId` | _required_ | Your store ID | | ||
| | `WebhookSigningSecret` | _required for webhooks_ | HMAC-SHA256 secret used by the webhook endpoint | | ||
| | `BaseUrl` | `https://api.lemonsqueezy.com/v1/` | API base URL | | ||
| | `TimeoutSeconds` | `30` | HTTP timeout | | ||
| | `MaxRetries` | `3` | Retry attempts on transient failures | | ||
| | `TestMode` | `false` | Whether to flag operations as test | | ||
|
|
||
| ## Usage | ||
|
|
||
| The adapter exposes: | ||
|
|
||
| - `IBillingService` — customers, subscriptions, checkouts (same shape as Stripe) | ||
| - `ILicenseService` — license key validation, activation, deactivation (LS-specific feature) | ||
|
|
||
| ```csharp | ||
| public sealed class ValidateLicenseHandler(ILicenseService licenses) | ||
| : IQueryHandler<ValidateLicenseQuery, LicenseValidationResult> | ||
| { | ||
| public Task<Result<LicenseValidationResult>> Handle( | ||
| ValidateLicenseQuery q, CancellationToken ct) | ||
| => licenses.ValidateLicenseAsync(q.LicenseKey, q.InstanceId, ct); | ||
| } | ||
| ``` | ||
|
|
||
| The license API is used for software activation flows — when you want to ship a downloadable app and gate it behind a key. | ||
|
|
||
| ## Gotchas | ||
|
|
||
| - **JSON:API for resources, plain JSON for license endpoints.** LemonSqueezy uses JSON:API for customers/subscriptions/checkouts, but its license endpoints (`/licenses/validate`, `/licenses/activate`, `/licenses/deactivate`) return plain JSON. The adapter handles both internally; don't be surprised if you debug the wire format and see two shapes. | ||
| - **License keys are secrets — `licenseKeyId` is not.** The `licenseKeyId` returned by the API identifies a license record (like a customer ID) and is fine to log. The `licenseKey` itself is sensitive; Compendium masks it via `GetKeyShort` in service-layer logs and never includes it in URL paths. | ||
| - **Activation limits.** A license key can only be activated on N machines (configured per product variant). Hitting the limit returns a specific error code; your UX should distinguish "invalid key" from "limit reached." | ||
| - **Webhook signature.** Set `WebhookSigningSecret` and verify HMAC in your webhook endpoint. Without it, anyone can POST events to your service. | ||
|
|
||
| ## See also | ||
|
|
||
| - [API Reference: Compendium.Adapters.LemonSqueezy.Configuration](../api/Compendium.Adapters.LemonSqueezy.Configuration.html) | ||
| - [LemonSqueezy docs](https://docs.lemonsqueezy.com/) | ||
| - [Stripe adapter](stripe.md) — same port, different provider |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,74 @@ | ||
| # Compendium.Adapters.Listmonk | ||
|
|
||
| > Coming soon — tracked in [POM-184](https://github.com/sassy-solutions/compendium/issues?q=is%3Aissue+POM-184). | ||
| [Listmonk](https://listmonk.app/) is a self-hosted newsletter and mailing list manager. This adapter implements the email-provider and newsletter-management ports from `Compendium.Abstractions.Email` against a Listmonk instance. | ||
|
|
||
| Listmonk newsletter adapter. | ||
| ## Install | ||
|
|
||
| ```bash | ||
| dotnet add package Compendium.Adapters.Listmonk | ||
| ``` | ||
|
|
||
| You need a Listmonk instance reachable from your service. | ||
|
|
||
| ## Configuration | ||
|
|
||
| ```json | ||
| { | ||
| "Listmonk": { | ||
| "BaseUrl": "https://listmonk.example.com", | ||
| "Username": "api-user", | ||
| "Password": "<long-random-secret>", | ||
| "DefaultFromEmail": "noreply@example.com", | ||
| "DefaultFromName": "Acme", | ||
| "DefaultListId": 1 | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Options (`ListmonkOptions`): | ||
|
|
||
| | Option | Default | Description | | ||
| |---|---|---| | ||
| | `BaseUrl` | _required_ | URL of the Listmonk instance | | ||
| | `Username` | _required_ | Basic-auth username | | ||
| | `Password` | _required_ | Basic-auth password (Listmonk does not expose API tokens — use a strong password) | | ||
| | `DefaultFromEmail` | _empty_ | Sender address for transactional emails | | ||
| | `DefaultFromName` | _empty_ | Sender display name | | ||
| | `DefaultListId` | `null` | Default list new subscribers are added to | | ||
| | `TimeoutSeconds` | `30` | HTTP timeout | | ||
| | `MaxRetries` | `3` | Retry attempts on transient failures | | ||
| | `SkipSslValidation` | `false` | Dev-only — never enable in production | | ||
|
|
||
| ## Usage | ||
|
|
||
| ```csharp | ||
| public sealed class SubscribeHandler(INewsletterService newsletter) | ||
| : ICommandHandler<SubscribeCommand> | ||
| { | ||
| public async Task<Result> Handle(SubscribeCommand cmd, CancellationToken ct) | ||
| { | ||
| var subResult = await newsletter.SubscribeAsync( | ||
| email: cmd.Email, | ||
| listId: cmd.ListId.ToString(), | ||
| attributes: new Dictionary<string, object> { ["source"] = "checkout" }, | ||
| ct); | ||
|
|
||
| return subResult.IsSuccess ? Result.Success() : subResult.Error; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| The adapter also exposes `IEmailService` for transactional sends, list management (`ListMailingListsAsync`, etc.), and bulk operations. | ||
|
|
||
| ## Gotchas | ||
|
|
||
| - **PII in logs.** Subscriber emails are sent to Listmonk (intended), but Compendium does not log raw emails server-side after [POM-178](https://github.com/sassy-solutions/compendium/pull/3). When extending this adapter, mirror the pattern: log `subscriber_id` post-lookup, never the raw email. | ||
| - **List IDs are integers.** Listmonk uses numeric list IDs; the adapter accepts `string` for portability and parses internally. Pass numeric strings. | ||
| - **Basic auth over HTTPS only.** Listmonk credentials over plain HTTP leak the password on every request. If `BaseUrl` starts with `http://`, fail fast in production. | ||
| - **Bulk operations are paginated.** Listing more than ~100 subscribers requires pagination — use the `page`/`per_page` parameters in the underlying client. | ||
|
|
||
| ## See also | ||
|
|
||
| - [API Reference: Compendium.Adapters.Listmonk.Configuration](../api/Compendium.Adapters.Listmonk.Configuration.html) | ||
| - [Listmonk docs](https://listmonk.app/docs/) | ||
| - [`Compendium.Abstractions.Email`](../api/Compendium.Abstractions.Email.html) — port contracts | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,5 +1,79 @@ | ||||||
| # Compendium.Adapters.OpenRouter | ||||||
|
|
||||||
| > Coming soon — tracked in [POM-184](https://github.com/sassy-solutions/compendium/issues?q=is%3Aissue+POM-184). | ||||||
| [OpenRouter](https://openrouter.ai/) is a unified API that proxies many LLM providers (Anthropic, OpenAI, Google, Mistral, Meta, …) behind a single OpenAI-compatible interface. This adapter implements the AI-provider port from `Compendium.Abstractions.AI` against OpenRouter. | ||||||
|
|
||||||
| OpenRouter AI provider adapter. | ||||||
| ## Install | ||||||
|
|
||||||
| ```bash | ||||||
| dotnet add package Compendium.Adapters.OpenRouter | ||||||
| ``` | ||||||
|
|
||||||
| You need an OpenRouter API key. | ||||||
|
|
||||||
| ## Configuration | ||||||
|
|
||||||
| ```json | ||||||
| { | ||||||
| "OpenRouter": { | ||||||
| "ApiKey": "sk-or-v1-...", | ||||||
| "DefaultModel": "anthropic/claude-3.5-sonnet", | ||||||
| "SiteUrl": "https://example.com", | ||||||
| "SiteName": "Acme" | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| Options (`OpenRouterOptions`): | ||||||
|
|
||||||
| | Option | Default | Description | | ||||||
| |---|---|---| | ||||||
| | `ApiKey` | _required_ | OpenRouter API key | | ||||||
| | `BaseUrl` | `https://openrouter.ai/api/v1` | API base URL | | ||||||
| | `DefaultModel` | `anthropic/claude-3.5-sonnet` | Default model when not specified per call | | ||||||
| | `DefaultTemperature` | `0.7` | Default temperature | | ||||||
| | `DefaultMaxTokens` | `4096` | Default max tokens | | ||||||
| | `TimeoutSeconds` | `120` | HTTP timeout (LLM calls can be slow) | | ||||||
| | `RetryAttempts` | `3` | Retries on transient failures | | ||||||
| | `SiteUrl` | `null` | Sent in `HTTP-Referer` for OpenRouter rankings | | ||||||
| | `SiteName` | `null` | Sent in `X-Title` for OpenRouter rankings | | ||||||
| | `EnableLogging` | `false` | Log full request/response (do not enable in prod) | | ||||||
| | `Models` | `{}` | Per-model overrides (max tokens, temperature, custom params) | | ||||||
|
|
||||||
| ## Usage | ||||||
|
|
||||||
| ```csharp | ||||||
| public sealed class SummarizeHandler(IAIProvider ai) | ||||||
| : IQueryHandler<SummarizeQuery, string> | ||||||
| { | ||||||
| public async Task<Result<string>> Handle(SummarizeQuery q, CancellationToken ct) | ||||||
| { | ||||||
| var result = await ai.CompleteAsync(new CompletionRequest | ||||||
| { | ||||||
| Model = "anthropic/claude-3.5-sonnet", | ||||||
| Messages = [new("user", q.Text)], | ||||||
|
||||||
| Messages = [new("user", q.Text)], | |
| Messages = [Message.User(q.Text)], |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,5 +1,87 @@ | ||||||
| # Compendium.Adapters.PostgreSQL | ||||||
|
|
||||||
| > Coming soon — tracked in [POM-184](https://github.com/sassy-solutions/compendium/issues?q=is%3Aissue+POM-184). | ||||||
| PostgreSQL-backed event store and projection store for Compendium. Designed for high-throughput SaaS workloads with proper connection pooling and bulk-insert support. | ||||||
|
|
||||||
| PostgreSQL event store and projection store. | ||||||
| ## Install | ||||||
|
|
||||||
| ```bash | ||||||
| dotnet add package Compendium.Adapters.PostgreSQL | ||||||
| ``` | ||||||
|
|
||||||
| You also need a PostgreSQL 14+ instance reachable from your service. | ||||||
|
|
||||||
| ## Configuration | ||||||
|
|
||||||
| ```json | ||||||
| { | ||||||
| "Compendium": { | ||||||
| "EventStore": { | ||||||
| "ConnectionString": "Host=localhost;Database=compendium;Username=app;Password=secret", | ||||||
| "AutoCreateSchema": false, | ||||||
| "BatchSize": 1000 | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ```csharp | ||||||
| builder.Services.Configure<PostgreSqlOptions>( | ||||||
| builder.Configuration.GetSection(PostgreSqlOptions.SectionName)); | ||||||
| ``` | ||||||
|
|
||||||
| Options (`PostgreSqlOptions`): | ||||||
|
|
||||||
| | Option | Default | Description | | ||||||
| |---|---|---| | ||||||
| | `ConnectionString` | _required_ | Standard Npgsql connection string | | ||||||
| | `MaxPoolSize` | `200` | Application-level concurrency cap (SemaphoreSlim) | | ||||||
| | `CommandTimeout` | `60s` | Single-query timeout | | ||||||
| | `TableName` | `event_store` | Event store table | | ||||||
| | `AutoCreateSchema` | `false` | Whether to auto-create tables on startup (dev only) | | ||||||
| | `BatchSize` | `1000` | Bulk-write batch size | | ||||||
| | `MinimumPoolSize` | `50` | Npgsql min pool — prewarmed connections | | ||||||
| | `MaximumPoolSize` | `200` | Npgsql max pool (must be ≤ Postgres `max_connections`) | | ||||||
| | `ConnectionIdleLifetime` | `900s` | Idle connection close timeout | | ||||||
| | `ConnectionLifetime` | `3600s` | Hard connection recycle | | ||||||
| | `ConnectionTimeout` | `30s` | Pool wait timeout | | ||||||
| | `Keepalive` | `30s` | TCP keepalive (`0` to disable) | | ||||||
| | `EnablePooling` | `true` | Disable only for debugging | | ||||||
|
|
||||||
| ## Usage | ||||||
|
|
||||||
| Once registered, the event store is available via the `IEventStore` port (from `Compendium.Abstractions`): | ||||||
|
|
||||||
| ```csharp | ||||||
| public sealed class PlaceOrderHandler(IEventStore eventStore) | ||||||
| : ICommandHandler<PlaceOrderCommand> | ||||||
| { | ||||||
| public async Task<Result> Handle(PlaceOrderCommand cmd, CancellationToken ct) | ||||||
| { | ||||||
| var orderResult = OrderAggregate.Create(cmd.CustomerId, cmd.Amount); | ||||||
| if (orderResult.IsFailure) return orderResult.Error; | ||||||
|
|
||||||
| await eventStore.AppendAsync( | ||||||
| orderResult.Value.Id.ToString(), | ||||||
| orderResult.Value.DomainEvents, | ||||||
| expectedVersion: 0, | ||||||
| ct); | ||||||
|
Comment on lines
+63
to
+67
|
||||||
|
|
||||||
| return Result.Success(); | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| 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, 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
INewsletterService.SubscribeAsynctakes aSubscribeRequestobject; there is no overload that acceptsemail,listId, andattributesas separate parameters. This example should constructnew SubscribeRequest { Email = ..., ListIds = [...], Attributes = ... }and pass that intoSubscribeAsync.