Skip to content

feat: add SaaS multi-tenant CQRS example + tenant-aware backend#579

Open
g-dumas wants to merge 26 commits into
hnaderi:mainfrom
beyond-scale-group:purring-notebook
Open

feat: add SaaS multi-tenant CQRS example + tenant-aware backend#579
g-dumas wants to merge 26 commits into
hnaderi:mainfrom
beyond-scale-group:purring-notebook

Conversation

@g-dumas
Copy link
Copy Markdown

@g-dumas g-dumas commented Apr 1, 2026

Summary

  • Add a complete Product Catalog example (ProductCatalogExample.scala) demonstrating custom ApiKeyContext auth with scopes, typed ProductRejection enum, and a product lifecycle state machine (Draft → Published → Archived → Deleted)
  • Add SaaS-aware backend tables with tenant_id and owner_id as first-class SQL columns:
    • TenantExtractor[S] typeclass to extract tenant/owner from CrudState
    • SaaSPGSchema for DDL generation with tenant indexes and optional Row-Level Security (RLS)
    • New saas-skunk module with SaaSSkunkCQRSDriver that persists tenant columns in states and outbox tables
  • Add 40 new tests (27 domain tests + 13 schema/extractor tests) — module coverage: 93.7% statements, 100% branches
  • Update SaaS tutorial with full SQL DDL examples, Flyway workflow, and test snippets

New module: edomata-saas-skunk

Drop-in replacement for SkunkCQRSDriver that adds tenant_id/owner_id columns:

import edomata.skunk.SaaSSkunkCQRSDriver

val driver = SaaSSkunkCQRSDriver.from[IO](PGNaming.prefixed("catalog"), pool)
val backend = driver.backend[CrudState[Product], ProductNotification, ProductRejection](
  handler = Some(ProductProjection.handler)
)

Generated DDL includes tenant-scoped indexes and optional RLS policies:

SaaSPGSchema.cqrs(naming, rls = Some(RLSConfig("app_user", "app.tenant_id")))

Test plan

  • sbt saasJVM/test — 116 tests passed (40 new + 76 existing), 0 failures
  • sbt saasSkunkJVM/compile — compiles successfully
  • sbt examplesJVM/compile — compiles successfully
  • sbt scalafmtAll — code formatted
  • Coverage: 93.7% statements, 100% branches on saas module
  • Integration test with real PostgreSQL (saas-skunk driver)

🤖 Generated with Claude Code

Guillaume DUMAS and others added 26 commits January 29, 2026 14:30
* Add CLAUDE.md with project guidance

Provides development setup, build commands, project structure, and module documentation for working with the Edomata codebase.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* Enrich tutorials for non-expert developers

Add beginner-friendly explanations throughout all 5 tutorials:

- 0_getting_started: Problem statement, architecture diagram, aggregate
  and pure function explanations, Edomaton vs Stomaton decision guide
- 1-1_eventsourcing: Event sourcing intro, Decision analogy, Scala
  syntax notes, ValidatedNec and fold explanations, testing benefits
- 1-2_cqrs: CQRS intro, state diagrams, EitherNec vs Decision
  comparison, Stomaton vs Edomaton decision matrix
- 2_backends: Backend concept, program/interpreter pattern, persistence
  tables explained, outbox and idempotency, minimal setup example
- 3_processes: Process intro, outbox pattern diagram, stream
  explanation, process manager and saga patterns with examples

Each technical term now includes real-world analogies and practical
context for developers unfamiliar with Scala, event sourcing, DDD,
or CQRS concepts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Convert ASCII diagrams to Mermaid in tutorials

Replace ASCII art diagrams with Mermaid for better rendering:

- 0_getting_started: Layer architecture flowchart
- 1-2_cqrs: Traditional vs CQRS comparison, order state diagram
- 2_backends: Pure/backend architecture, database ER diagram
- 3_processes: System architecture, outbox sequence diagram, saga flow

Mermaid diagrams render properly in GitHub, documentation sites,
and most modern markdown viewers.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: GitHub Actions Bot <actions@github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Adds fork-aware ticket workflow instructions so Claude Code
automatically detects fork vs root repo and targets PRs accordingly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
docs: add contribution workflow to CLAUDE.md
Adds context7.json so Edomata can be indexed on Context7, enabling
AI coding assistants (Claude Code, Cursor, Copilot) to look up
Edomata documentation directly.

Fixes hnaderi#573

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add PGNaming sealed trait with Schema and Prefixed variants, allowing
tables to use a name prefix (e.g. auth_journal) instead of a dedicated
PostgreSQL schema (e.g. "auth".journal). This is useful for projects
using Flyway migrations in a single schema.

API: PGNamespace.prefixed("auth") or PGNaming.prefixed("auth")

Fixes hnaderi#574

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add SkunkPrefixedPersistenceSuite and SkunkPrefixedCQRSSuite
- Add DoobiePrefixedPersistenceSuite and DoobiePrefixedCQRSSuite
- Document table prefix mode in skunk.md, doobie.md, and backends tutorial

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add PGSchema utility to generate DDL SQL strings for migration files
  (eventsourcing and cqrs variants with configurable payload types)
- Add skipSetup parameter to all driver .from() methods to disable
  automatic schema/table creation at startup
- Add 10 unit tests for PGSchema DDL generation
- Document Flyway workflow in skunk.md and doobie.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add comprehensive AI agent documentation covering:
- Table naming strategies (Schema vs Prefixed)
- DDL extraction with PGSchema for Flyway workflows
- skipSetup parameter for disabling automatic DDL
- Guide for adding new tables with proper naming flow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CLAUDE.md: SBT 1.12.1 -> 1.12.8, Doobie RC11 -> RC12,
  docker credentials test/test -> postgres/postgres
- tutorials/2_backends.md: replace non-existent SkunkBackend API
  with actual Backend.builder().use(SkunkDriver(...)) pattern,
  fix connection credentials

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a pre-PR documentation audit process to CLAUDE.md that AI agents
must run before opening any PR. Includes a detailed audit prompt that
checks CLAUDE.md, backend docs, tutorials, and features against actual
source code (versions, API signatures, file paths, code examples).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
examples/ is a root-level directory, not under modules/.
Found by documentation audit agent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add language requirement: all commits, PRs, comments, docs, and branch
names must be in English for this international open-source library.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
docs: add Context7 documentation indexing
docs: add expert-flow.ai to adopters
feat: table prefix mode, DDL extraction, and Flyway support
…ards

Introduces edomata-saas, a cross-platform module providing generic
multi-tenant CRUD abstractions. Services extending the SaaS base traits
get automatic tenant isolation and role-based authorization before any
business logic runs, for both event-sourcing and CQRS approaches.

Key features:
- SaaSDomainDSL / SaaSCQRSDomainDSL with guardedRouter (auto-guarded)
  and unsafeUnguardedRouter (explicit bypass for super-admin)
- CrudState[A] enum with tenant+owner tracking
- TenantScopedQuery / UnsafeCrossTenantQuery for read-side enforcement
- Selective re-exports in package.scala to prevent raw DSL usage
- 15 unit tests for guard logic
- Documentation page (docs/tutorials/4_saas.md)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Full Todo CQRS example showing: domain types, SaaS service with
  guardedRouter, SkunkHandler for SQL read-model projection,
  TenantScopedQuery for tenant-scoped reads, admin bypass with
  unsafeUnguardedRouter, and backend wiring
- Integration tests exercising guardedRouter through the Stomaton
  pipeline: tenant mismatch rejection, missing roles rejection,
  Create bypass, unsafe bypass, and notification emission
- 9 new tests (24 total for the saas module)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New test suites:
- SaaSDomainDSLSuite: 24 tests covering the event-sourcing guarded DSL
  (guardedRouter, unsafeUnguardedRouter, guarded, unsafeUnguarded,
  caller/command/entityState/aggregateId accessors, pure, unit, reject,
  decide, publish, validate, notifications, Deleted state handling)
- TenantAwareReaderSuite: 8 tests covering read-side abstractions
  (TenantScopedQuery factory and tenant filtering, UnsafeCrossTenantQuery,
  TenantAwareReader trait implementation)
- SaaSServiceSuite: 20 tests covering service traits, CQRS DSL methods,
  event-sourced service, CrudState patterns, types, and package re-exports
  (domain field, set, modifyS, decideS, eval, DomainModel transitions,
  TenantId/UserId round-trips, SaaSCommand covariance, CrudAction enum,
  CommandMessage/Decision/MessageMetadata re-exports)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace hardcoded CallerIdentity with generic Auth type parameter
throughout the SaaS module. Authorization logic is now defined by
implementing the AuthPolicy[Auth] trait, which provides:
- tenantId(auth): extract tenant from any auth context
- authorize(auth, action): custom authorization check

CallerIdentity remains as a default implementation, with
RoleBasedPolicy for role-based access control.

Changes:
- Add AuthPolicy[Auth] trait and RoleBasedPolicy class
- SaaSCommand[C] -> SaaSCommand[Auth, C]
- DSLs, services, readers all parameterized by Auth
- SaaSGuard.checkRoles -> SaaSGuard.checkAuthorization (delegates to policy)
- caller accessor -> auth accessor
- rolesFor param removed from DSL/Service constructors (moved to AuthPolicy)
- Tests updated + custom AuthPolicy test (ApiKeyAuth example)
- 77 tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite documentation to reflect the generic auth refactor:
- Add AuthPolicy typeclass section with full API
- Add Custom Auth Types section with JwtClaims, ApiKeyAuth examples
- Update all code samples: SaaSCommand[Auth, C], auth accessor,
  RoleBasedPolicy, TenantScopedQuery with Auth type param
- Rename "Role check" to "Authorization check" throughout
- Update overview to emphasize pluggable authorization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add multi-tenant SaaS CRUD module
…ions

Add a complete Product Catalog example demonstrating:
- Custom AuthPolicy (ApiKeyContext with scopes, not CallerIdentity)
- Typed rejections (ProductRejection enum, not String)
- Product lifecycle state machine (Draft → Published → Archived → Deleted)
- SQL DDL for CQRS backend tables and custom read-model projection
- Flyway migration workflow with skipSetup

Include 27 pure domain unit tests covering CRUD lifecycle, state machine
validation, scope-based authorization, multi-tenant isolation, admin
bypass, notification emission, and price validation.

Update SaaS tutorial with full Product Catalog section including
generated SQL examples, custom auth patterns, and test snippets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add tenant-aware database tables to the SaaS module, enabling
Row-Level Security, tenant-scoped indexes, and SQL-level audit queries.

Changes:
- Add TenantExtractor typeclass to extract tenant/owner from CrudState
- Add SaaSPGSchema for DDL generation with tenant_id, owner_id columns,
  tenant indexes, and optional RLS policies
- Create saas-skunk module with SaaSSkunkCQRSDriver that persists
  tenant_id/owner_id as first-class columns in states and outbox tables
- Update ProductCatalogExample to use SaaS-aware driver
- Add 13 new tests (TenantExtractor + SaaSPGSchema DDL validation)

Test coverage: 93.7% statements, 100% branches on saas module.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@g-dumas g-dumas changed the title feat: add Product Catalog multi-tenant CQRS example feat: add SaaS multi-tenant CQRS example + tenant-aware backend Apr 1, 2026
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