Aller au contenu

Papillon Collection Solution — SaaS Architecture Blueprint

Reusing the AWS SaaS Reference Architecture — Cloud-Agnostic, Adapted to Renew Insurance V1


1. What We're Borrowing from AWS SaaS Factory (and What Changes)

The AWS SaaS Factory's Control Plane / Application Plane decomposition is the right backbone. Implementation is heavily adapted to our locked stack and to V1 scope (small brokerages in Côte d'Ivoire, single-product, low-bandwidth customer flow).

What stays the same (architectural patterns)

  • Two-plane model: Control Plane (platform/technical) + Application Plane (commercial/business)
  • Event-driven tenant onboarding
  • Tiered isolation (pooled by default, BYO-DB when an enterprise tenant requires it)
  • Tenant context propagation from edge to data store
  • Tenant-aware logging and metrics

What changes (implementation)

AWS Pattern AWS Implementation Papillon Implementation
Control Plane ↔ Application Plane communication EventBridge (cross-service) Spring Application Events in-process, same JVM. Single deployable for V1 — no message broker on the critical path.
Microservices per concern Lambda / ECS per service Spring Modulith modules in a single deployable. Boundaries enforced via package structure + module-test verification, not network.
Tenant resolver Cognito + API Gateway header Operator side: Keycloak JWT → Spring Security filter → TenantContext (ScopedValue + ThreadLocal fallback) → tenant-aware datasource resolver. Customer side: signed link decodes to tenant context server-side.
Schema provisioning CDK/CloudFormation per tenant Flyway + a custom provisioner triggered by Application Events. V1 = shared schema; BYO-DB validates and pools an externally-supplied connection.
Per-tenant cost / usage metering CloudWatch + custom metrics Micrometer → Prometheus with tenant_id tag on every metric.

2. Architecture Overview — Aligned with Our Stack

┌──────────────────────────────────────────────────────────────────────────┐
│                  PAPILLON COLLECTION SOLUTION                            │
│                  (Single Spring Boot 4 application)                      │
│                  (Spring Modulith — modular monolith)                    │
│                                                                          │
│  ┌────────────────────────────────┐  ┌─────────────────────────────────┐ │
│  │     CONTROL PLANE              │  │     APPLICATION PLANE           │ │
│  │     (technical / platform)     │  │     (commercial / business)     │ │
│  │     com.altarys.papillon.pcs.controlplane.*    │  │     com.altarys.papillon.pcs.*      │ │
│  │                                │  │                                 │ │
│  │  ┌──────────────────────────┐  │  │  ┌───────────────────────────┐ │ │
│  │  │ tenant/         (TENANT) │  │  │  │ ingestion/         (ING)  │ │ │
│  │  │  Tenant + agency CRUD    │  │  │  │  CSV/XLSX import          │ │ │
│  │  │  Country profile lookup  │  │  │  │  Validation + dedup       │ │ │
│  │  │  Onboarding orchestrator │  │  │  ├───────────────────────────┤ │ │
│  │  ├──────────────────────────┤  │  │  │ customer-portal/    (CP)  │ │ │
│  │  │ identity/       (AUTH)   │  │  │  │  Signed-link landing      │ │ │
│  │  │  Keycloak (operator)     │  │  │  │  Resumable, low-bandwidth │ │ │
│  │  │  Signed-link issuer      │  │  │  ├───────────────────────────┤ │ │
│  │  │  (customer side)         │  │  │  │ operator-console/  (OP)   │ │ │
│  │  ├──────────────────────────┤  │  │  │  Back-office UI           │ │ │
│  │  │ billing/        (BILL)   │  │  │  │  Campaign engine (CAMP    │ │ │
│  │  │  Platform → tenant       │  │  │  │  folded in: cohorts,      │ │ │
│  │  │  invoicing               │  │  │  │  schedules, send-via-     │ │ │
│  │  │  FNE / e-invoicing       │  │  │  │  notifications)           │ │ │
│  │  │  per country profile     │  │  │  ├───────────────────────────┤ │ │
│  │  ├──────────────────────────┤  │  │  │ payment/          (PAY)   │ │ │
│  │  │ audit/         (AUDIT)   │  │  │  │  Customer → broker        │ │ │
│  │  │  Immutable trail         │  │  │  │  Provider integration     │ │ │
│  │  │  V1 PRD §18 events       │  │  │  │  Webhooks + reconciliation│ │ │
│  │  ├──────────────────────────┤  │  │  │  Idempotency keys         │ │ │
│  │  │ notification/  (NOTIF)   │  │  │  └───────────────────────────┘ │ │
│  │  │  SMS / WhatsApp adapters │  │  │                                 │ │
│  │  │  Delivery + retry + DLR  │  │  │                                 │ │
│  │  └──────────────────────────┘  │  │                                 │ │
│  └────────────────────────────────┘  └─────────────────────────────────┘ │
│                                                                          │
│  ┌──────────────────────────────────────────────────────────────────────┐│
│  │ SHARED INFRASTRUCTURE LAYER  (com.altarys.papillon.pcs.controlplane.infrastructure)  ││
│  │                                                                      ││
│  │  TenantContext │ TenantAwareDataSource │ AuditService │ i18n         ││
│  │  EventBus (Spring App Events) │ SecurityConfig │ SignedLinkSigner    ││
│  └──────────────────────────────────────────────────────────────────────┘│
│                                                                          │
│  ════════════════════════════════════════════════════════════════════════ │
│                                                                          │
│  ┌─────────────────────────┐        ┌─────────────────────────────┐      │
│  │  customer-portal/        │        │  operator-console/           │     │
│  │  React 19.2 / TS         │        │  React 19.2 / TS             │     │
│  │  Mobile-first, 360px     │        │  Desktop-first, dense        │     │
│  │  < 200 KB gzipped initial│        │  Online-first, min offline   │     │
│  │  No PWA / SW / IndexedDB │        │  base from V1 PRD §20        │     │
│  │  Signed-link entry       │        │  Keycloak auth               │     │
│  └─────────────────────────┘        └─────────────────────────────┘      │
└──────────────────────────────────────────────────────────────────────────┘

External services:
┌────────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ ┌────────────────────┐
│  Keycloak  │ │ Postgres │ │  Redis   │ │ MinIO  │ │ Mobile-money       │
│  (operator │ │  (multi- │ │ (cache,  │ │(quitt- │ │ providers          │
│  IAM)      │ │  tenant) │ │ rate-lim)│ │ ances) │ │ (Orange Money, etc)│
└────────────┘ └──────────┘ └──────────┘ └────────┘ └────────────────────┘

Why a single deployable (not microservices)

  • Small team. Operating N microservices is infrastructure tax we can't afford.
  • Spring Modulith gives module boundaries without network overhead. Each module is package-isolated, communicates via Spring Application Events, and can be extracted later if scale demands it.
  • Single Coolify deployment: one Docker image, one PostgreSQL, one Keycloak.

3. AWS-to-Stack Mapping

3.1 Identity & Access

AWS Service Papillon Replacement Notes
Amazon Cognito Keycloak 26+ (operator side only) OIDC/OAuth2. French locale. JWT with tenant_id, agency_id, country_code claims.
Cognito User Pool per tier Single Keycloak realm in V1 Tenant carried as JWT claim. Realm-per-tenant deferred to V2 if a silo enterprise asks.
(No AWS equivalent) Signed-link scheme (customer side) Server-secret-signed, 48–72h TTL, attempt-limited, anti-replay. No Keycloak account per insured.

3.2 API Gateway & Routing

AWS Service Papillon Replacement Notes
Amazon API Gateway Spring Boot embedded Single app — Spring Security filters extract tenant context. Spring Cloud Gateway only if/when the monolith is split.
CloudFront Coolify reverse proxy + Cloudflare free CDN Serves React bundles, handles TLS.

Tenant resolution (operator side)

// 1. JWT filter extracts tenant from Keycloak token
@Component
public class OperatorTenantResolutionFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, ...) {
        String tenantId    = jwtDecoder.extractClaim(request, "tenant_id");
        String agencyId    = jwtDecoder.extractClaim(request, "agency_id"); // nullable
        String countryCode = jwtDecoder.extractClaim(request, "country_code");
        TenantContext.set(tenantId, agencyId, countryCode);
        try { filterChain.doFilter(request, response); }
        finally { TenantContext.clear(); }
    }
}

Tenant resolution (customer side)

// Signed link: /portail/{token}
// Token decodes to {tenantId, agencyId, contractId, expiresAt, nonce, sig}
@Component
public class SignedLinkResolutionFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, ...) {
        SignedLinkClaims claims = signedLinkVerifier.verify(extractToken(request));
        // verify(): signature, expiry, attempt count, nonce-replay, kill-switch
        TenantContext.set(claims.tenantId(), claims.agencyId(), claims.countryCode());
        CustomerContext.set(claims.contractId());
        try { filterChain.doFilter(request, response); }
        finally { CustomerContext.clear(); TenantContext.clear(); }
    }
}

TenantContext (Java 26)

public final class TenantContext {
    private static final ScopedValue<TenantInfo> SCOPED  = ScopedValue.newInstance();
    private static final ThreadLocal<TenantInfo> FALLBACK = new ThreadLocal<>();

    public record TenantInfo(String tenantId, String agencyId, String countryCode) {}

    public static TenantInfo current() {
        if (SCOPED.isBound()) return Objects.requireNonNull(SCOPED.get());
        return Objects.requireNonNull(FALLBACK.get(), "TenantContext not set");
    }
    public static void set(String t, String a, String c) {
        FALLBACK.set(new TenantInfo(t, a, c));
    }
    public static void clear() { FALLBACK.remove(); }
}

ScopedValue for HTTP requests on virtual threads; ThreadLocal fallback for @Scheduled, @Async, Flyway, tests.

Spring Data JDBC repositories (no Hibernate magic)

Every query MUST include tenant_id as a parameter — no CurrentTenantIdentifierResolver-style auto-injection. This is better for security: no magic, no risk of forgetting the filter, every query is auditable.

public interface ContractRepository extends Repository<Contract, Long> {
    @Query("SELECT * FROM contract WHERE tenant_id = :tenantId AND id = :id")
    Optional<Contract> findByIdAndTenant(@Param("id") Long id,
                                         @Param("tenantId") String tenantId);
}

3.3 Compute & Containers

AWS Service Papillon Replacement
ECS / Fargate Docker + Coolify (self-hosted Vercel-alike — TLS, reverse proxy, health checks, rollbacks).
Lambda Not used. Modulith modules run in-process.
CodePipeline GitHub Actions → Docker image → Coolify webhook.

3.4 Data & Multi-Tenant Strategy (V1: 2 Profiles)

CLAUDE.md locks V1 to two profiles. Schema-per-tenant and DB-per-tenant are explicitly V2.

Profile AWS Equivalent When Used V1? Implementation
Shared DB / shared schema Pool model Default for all V1 tenants tenant_id (+ agency_id) column on every tenant-scoped table. Mandatory in every WHERE clause.
BYO-DB (Not in AWS reference) Tenant supplies their own PostgreSQL Tenant-provided connection string, validated, encrypted at rest, pooled per tenant. Schema identical to shared.
Schema-per-tenant Bridge model V2 The resolver path is generic from day one so V2 adds an implementation, not a migration of tenant-scoped tables.
DB-per-tenant Silo model V2+ Same.
public interface TenantAwareDataSource {
    Connection getConnection(String tenantId);
}

public class SharedDbDataSource implements TenantAwareDataSource {
    private final DataSource pool;
    public Connection getConnection(String tenantId) { return pool.getConnection(); }
}

public class ByoDbDataSource implements TenantAwareDataSource {
    private final TenantConfigRepository config;
    private final Map<String, DataSource> cached = new ConcurrentHashMap<>();
    public Connection getConnection(String tenantId) {
        return cached.computeIfAbsent(tenantId, this::createPool).getConnection();
    }
    private DataSource createPool(String tenantId) {
        TenantDbConfig c = config.getDbConfig(tenantId);
        return HikariDataSourceBuilder.create()
            .jdbcUrl(c.jdbcUrl()).username(c.username()).password(c.password())
            .maximumPoolSize(c.poolSize()).build();
    }
}

A TenantDataSourceRouter resolves the right TenantAwareDataSource per request based on the tenant's profile.

3.5 Messaging & Events

Level Mechanism Scope
Intra-app Spring Application Events (ApplicationEventPublisher) Between Modulith modules in the same JVM (e.g. PaymentSucceededaudit/, notification/).
Async / out-of-process DB-backed job queue (spring-modulith-events-jdbc) → workers Webhook reconciliation retries, SMS/WhatsApp dispatch, signed-link expiry sweeps. No external broker in V1.

3.6 Other Infrastructure

AWS Service Papillon Replacement
S3 MinIO (Docker). Quittances PDF, receipts, ingestion source files (retention per CIMA + ARTCI).
ElastiCache Redis (Docker). Tenant config cache, signed-link rate-limit / attempt-count, idempotency keys.
CloudWatch Prometheus + Grafana via Micrometer with tenant_id tag.
X-Ray Micrometer Tracing (built into Spring Boot 4).

4. Tenant Onboarding

V1 has two entry points (no public self-onboard, no CRM module). Both converge on the same provisioning pipeline — the AWS SBT pattern, just with fewer channels.

4.1 Entry points

Channel Auth Purpose
Operator-driven (admin) Platform admin JWT Day-to-day onboarding of brokerage tenants.
Sales-driven (manual handoff) Platform admin JWT (for V1 — sales hands signed contract to admin) Same code path as admin; the salesperson does not have a separate UI in V1.

V2 may introduce a self-onboarding form once we have a public marketing site and CAPTCHA infra. Designing for it now adds risk surface (rate limits, email verification flow, abuse mitigation) we don't need in V1.

4.2 Unified provisioning pipeline

TenantOnboardingService.onboard(request)
    ├── 1. Validate input (legal name, country_code, primary contact)
    ├── 2. Resolve country profile (CI for V1)
    ├── 3. Persist Tenant (status = PENDING) + default Agency
    ├── 4. Publish TenantOnboardingRequested(tenantId, profile, country)
TenantProvisioningListener
    ├── Provision DB:
    │     ├── Shared DB → no DB action; tenant_id column handles it
    │     └── BYO-DB    → validate connection + run Flyway migrations
    ├── Seed country-specific reference data (V1: CI only)
    │     ├── Mobile-money provider config (Orange Money CI, Wave, MTN MoMo CI…)
    │     ├── E-invoicing config (FNE for CI; SN/BJ deferred to V2)
    │     ├── Currency: XOF
    │     └── Working calendar / holidays
    ├── Provision Keycloak (operator side):
    │     ├── Tenant client + default roles (Admin, Operator, Viewer)
    │     └── Initial admin user
    ├── Send activation email to tenant admin
    └── Publish TenantOnboarded → Tenant.status = ACTIVE

4.3 Tenant lifecycle

PENDING ──provisioned──► ACTIVE ──suspended──► SUSPENDED ──grace expired──► DELETED (soft)
                                ◄─reactivated─┘

Soft-delete only — CIMA + ARTCI retention obligations forbid hard-delete on contract / payment / audit data.

4.4 Country-specific behavior (lives on the tenant)

Per CLAUDE.md "Multi-Tenant Model":

Concern Resolution path
Mobile-money provider(s) and APIs tenant → country_code → country profile → provider list
Payment-time tax info (CM, GA…) country profile (V2+)
E-invoicing for platform → tenant billing country profile: FNE for CI (V1); equivalents in SN, BJ (V2+)
Personal-data authority country profile: ARTCI / CDP / APDP (informational; consent flows reference it)
Currency, holidays, working week country profile

V1 ships with a CI profile. The lookup path is wired from day one, even though only one row exists.

4.5 Items for ANALYST PRD interview (not architecture decisions)

  1. BYO-DB eligibility: which tenant tier / contract clause unlocks it?
  2. Onboarding email content per channel (copy + branding).
  3. Suspension grace period (days) before retention-only mode.
  4. What triggers an automatic suspension (non-payment threshold? duration?).
  5. Agency creation: at onboarding (single default) or post-activation by tenant admin?

5. Modules — Mapped to CLAUDE.md Prefix Registry

Module IDs come from the CLAUDE.md registry (LOCKED). The plane assignment below follows the technical-vs-commercial axis.

5.1 Control Plane (technical / platform)

Prefix Module Package Notes
TENANT tenant-admin com.altarys.papillon.pcs.controlplane.tenant Tenant + agency CRUD, lifecycle, country profile.
AUTH* identity com.altarys.papillon.pcs.controlplane.identity Keycloak integration (operator) + signed-link signer/verifier (customer).
BILL* billing com.altarys.papillon.pcs.controlplane.billing Platform → tenant invoicing. FNE integration for CI tenants. Plan/tier metering.
AUDIT audit-log com.altarys.papillon.pcs.controlplane.audit Immutable trail of every V1 PRD §18 event.
NOTIF notifications com.altarys.papillon.pcs.controlplane.notifications SMS / WhatsApp delivery primitive. Adapter-per-provider, retry, DLR tracking.

*AUTH and BILL are not yet in the CLAUDE.md prefix registry. A follow-up PR adds them and removes CAMP (folded into OP); merging that PR is a prerequisite to the first ANALYST session for either new module.

5.2 Application Plane (commercial / business)

Prefix Module Package Notes
ING ingestion com.altarys.papillon.pcs.ingestion CSV/XLSX upload, validation, dedup, contract canonicalization.
CP customer-portal com.altarys.papillon.pcs.customer Signed-link landing, consult-and-pay flow, low-bandwidth + resumable.
OP operator-console com.altarys.papillon.pcs.operator Back-office UI including the campaign engine (cohort selection, scheduling, send-via-NOTIF). CAMP merged in for V1 — split out only if a real rules engine emerges in V2.
PAY payment com.altarys.papillon.pcs.payment Customer → broker payment. Provider integration, webhooks, reconciliation, idempotency.

Why CAMP belongs in the application plane (and not merged with NOTIF)

NOTIF is a technical primitive: SMS / WhatsApp adapters, retry, DLR. It is reused across the platform — tenant onboarding emails (TENANT), audit alerts (AUDIT), signed-link delivery (CP/AUTH), AND campaign sends. That makes it a control-plane concern: every plane consumes it; no plane owns it.

CAMP is a business concept: which contracts are due, when to nudge, on which cadence, with what French copy. It is configured by the operator in the back-office UI and is tenant-scoped (different brokerages run different campaign rules). Merging it into NOTIF would smear business logic into a primitive that other modules already depend on, and would force every non-campaign sender (onboarding email, audit alert) to either route through campaign-shaped logic or bypass it. Both options are bad.

Folding CAMP into OP instead has clean module boundaries: OP owns the UI + the campaign tables + the cohort/schedule logic, and calls into NOTIF to actually send. If V2 introduces a real rules engine (segmentation, branching, multi-step journeys), CAMP can split back out as a sibling app-plane module without rewriting NOTIF.

5.3 Module dependency rules

  • App-plane modules may depend on Control-plane infrastructure (AUDIT, NOTIF, AUTH, TENANT lookups) and on each other only via published events.
  • Control-plane modules must not depend on app-plane modules. (Verified by ApplicationModules.verify().)
  • BILL (platform → tenant) and PAY (customer → broker) are deliberately separate. Different actors, different providers, different audit semantics.

6. Hosting (Coolify)

Phase Infrastructure Notes
V1 dev / pilot 1× Hetzner CX42 (8 vCPU / 16 GB) + Coolify App + Postgres + Keycloak + Redis + MinIO as Docker services on the same box. Auto-TLS via Let's Encrypt. Nightly Postgres dumps to a Storage Box.
V1 production 2× Hetzner (app + DB) Postgres on a dedicated server. Daily backups + WAL archiving.
V2 expansion +1 server per silo / per CEMAC region if data residency demands Country profile may force per-region DB instance for CM/GA tenants.

7. Key Patterns to Implement

7.1 Tenant-aware repository (most important pattern)

Manual tenant_id in every query. The REVIEWER blocks merge if missing.

public interface ContractRepository extends Repository<Contract, Long> {
    @Query("""
        SELECT * FROM contract
        WHERE tenant_id = :tenantId
          AND agency_id = :agencyId
          AND status IN (:statuses)
        ORDER BY expires_at ASC
        """)
    List<Contract> findExpiring(@Param("tenantId") String tenantId,
                                @Param("agencyId") String agencyId,
                                @Param("statuses") List<String> statuses);
}

@Service
public class RenewalService {
    public Contract markPaid(Long contractId) {
        var ctx = TenantContext.current();
        var contract = repo.findByIdAndTenant(contractId, ctx.tenantId())
            .orElseThrow(() -> new ResourceNotFoundException("contract", contractId));
        contract.markPaid();
        repo.save(contract);

        events.publishEvent(new PaymentReconciled(
            ctx.tenantId(), ctx.agencyId(), contract.id(), contract.amountMinor()));

        auditService.log("PAYMENT_RECONCILED", "Contract", contract.id());
        return contract;
    }
}

7.2 Modulith boundary verification

@ApplicationModuleTest
class ModuleBoundariesTest {
    @Test
    void verify() { ApplicationModules.of(PapillonApplication.class).verify(); }
}

7.3 Country configuration engine

@Service
public class CountryProfileService {
    public PaymentProviderConfig paymentProvider(String countryCode) { ... }
    public EInvoicingConfig      eInvoicing(String countryCode)      { ... }
    public Currency              currency(String countryCode)        { ... }
}

V1 seeds CI only. Adding BJ, SN, CM, GA (V2) is a data change, not a code change.

The customer-side equivalent of an auth subsystem. Lives in identity/ (control plane) so customer-portal only consumes it.

public interface SignedLinkSigner {
    String issue(SignedLinkClaims claims, Duration ttl);
}
public interface SignedLinkVerifier {
    SignedLinkClaims verify(String token); // signature, expiry, attempt count, nonce, kill-switch
}

Backed by HMAC-SHA-256 with a server-side secret. Attempt count + nonce stored in Redis. Kill-switch in Postgres for compliance audit. Spec details belong in docs/standards/connectivity-guidelines.md and docs/standards/security-logging-guidelines.md (to be created).

7.5 Tenant-aware MDC logging

@Component
public class TenantMdcFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, ...) {
        var ctx = TenantContext.current();
        MDC.put("tenant_id", ctx.tenantId());
        MDC.put("agency_id", String.valueOf(ctx.agencyId()));
        MDC.put("country_code", ctx.countryCode());
        try { filterChain.doFilter(req, res); }
        finally { MDC.clear(); }
    }
}
// Logback: %d{ISO8601} [%X{tenant_id}/%X{agency_id}] [%X{country_code}] %level %msg%n

7.6 Payment idempotency + reconciliation

  • Every payment-initiating endpoint accepts an Idempotency-Key header. Stored in Redis (TTL 24 h) + Postgres (long-term audit).
  • Webhook handlers verify the provider's signature before any side effect.
  • Reconciliation worker polls provider status for any payment in PENDING for > N minutes (country-profile-dependent), and reconciles via the same idempotent path the webhook would have taken.
  • A delayed-return payment never double-charges: the second arrival short-circuits on the idempotency key.

8. What's Renew-Specific (Not in AWS Blueprint)

Capability Source Implementation
CIMA country profile engine CLAUDE.md "Multi-Tenant Model" Lookup-table-driven, seeded with CI in V1.
Signed-link customer flow CLAUDE.md rule #7, V1 PRD §30 No Keycloak account per insured. HMAC + Redis state.
Low-bandwidth + resumable (NOT offline-first) V1 PRD §30, CLAUDE.md rule #11 < 200 KB gzipped initial customer payload. SSR/streaming where it helps. No PWA / SW / IndexedDB on customer side.
Mobile-money payment + reconciliation CLAUDE.md rules #8, #9 Provider adapter per country. Idempotency keys + webhook signature verification + status polling.
FNE e-invoicing for platform → tenant billing CLAUDE.md "Multi-Tenant Model" BILL module integrates DGI FNE for CI tenants. SN/BJ deferred.
Audit trail for V1 PRD §18 events CLAUDE.md rule #4 AUDIT module — immutable, append-only, tenant-scoped.
7-personality Claude workflow docs/AI_Development_Workflow.md LEGAL_AUDITOR → ANALYST → ARCHITECT + DESIGNER → PRODUCT_OWNER → DEVELOPER → REVIEWER.
French-first UX CLAUDE.md "Critical Rules" react-i18next, fr default, XOF with 0 decimals + space thousands separator.
Operator side = online-first with V1 PRD §20 minimum offline base V1 PRD §20 Operator may use a service worker; customer side may not.

9. Risks & Assumptions

# Assumption Risk if wrong Mitigation
1 Manual tenant_id in every query is sustainable across 9 modules Forgotten WHERE clause = cross-tenant leak REVIEWER checklist enforces it. Add an integration test that runs every repository method against a 2-tenant fixture and asserts isolation.
2 Single Spring Boot app handles V1 load (small brokerages, ~hundreds of contracts/day per tenant) Bottleneck under campaign blasts Java 26 virtual threads handle I/O concurrency well. NOTIF dispatch goes through the JDBC job queue, not the request thread. Monitor via Prometheus.
3 Coolify is sufficient for production Operational gaps (no auto-scaling, limited multi-region) V1 has zero need for multi-region. K8s migration deferred until tenant volume justifies it.
4 Shared DB scales through V1 Table sizes grow, queries slow Partial indexes on (tenant_id, ...). PgBouncer for connection pooling. V2 introduces schema-per-tenant before this hurts.
5 BYO-DB is V1 scope, not V2 Late add costs schema rework on tenant-scoped tables V1 implements the resolver path from day one — only the ByoDbDataSource impl is wired. Adding BYO at V1.x is a config change, not a migration.
6 Signed-link state in Redis tolerates Redis loss Active customer flows broken Attempt counts and nonces are not load-bearing if lost (the link is still HMAC-valid; lost attempt count just resets — within the 48–72 h TTL the abuse window is bounded). Critical state (kill-switch, audit) lives in Postgres.
7 Keycloak co-resident with app on V1 dev box Resource contention Keycloak is ~512 MB. Move to dedicated container if oom_kill shows up.
8 FNE integration available and stable for CI tenants in V1 Platform → tenant billing blocked at first FNE-required invoice Integrate FNE in BILL from the first invoice, not retroactively. Pilot CI tenants are CI-domiciled — there is no "later" for this.

10. How This Blueprint Fits the 7-Personality Workflow

Personality How they use this document
LEGAL_AUDITOR References §4.4 (country profile) and §3.1 (signed-link) when auditing memos that touch ARTCI, CIMA Reg. 01-24, or FNE.
ANALYST References §5 (module list) and §4 (onboarding) when writing module PRDs. Uses the control/application plane vocabulary.
ARCHITECT This document is pre-input for the ARCHITECT. Per-module architecture (docs/architecture/<module>-arch.md) refines the patterns here.
DESIGNER References §2 (frontends) — knows there are two distinct apps, not a dual-brand split.
PRODUCT_OWNER Uses §5 (module + dependency rules) when sequencing stories. Tags every story [V1].
DEVELOPER References §7 (key patterns) — tenant-aware repo, Modulith verification, signed-link, idempotency, MDC. Reads only the story file, never this blueprint directly during implementation.
REVIEWER Uses §3 (tenant resolution) and §7.1 (manual tenant_id) as the multi-tenant correctness checklist.

Document layout

docs/architecture/
├── platform-saas-blueprint.md   ← THIS DOCUMENT (cross-module backbone)
├── PROGRESS.md                   ← Cross-module decision log
├── tenant-arch.md                ← per-module ARCHITECT output
├── customer-portal-arch.md
├── payment-arch.md
└── ...

11. Summary

Papillon Collection Solution is a Spring Modulith modular monolith with two planes — a Control Plane (TENANT, AUTH, BILL, AUDIT, NOTIF) and an Application Plane (ING, CP, OP-with-CAMP-folded-in, PAY) — using Spring Data JDBC with explicit tenant_id in every query, supporting two V1 isolation profiles (Shared DB by default, BYO-DB for enterprise tenants), with Keycloak on the operator side and signed links on the customer side, mobile-money payment with idempotency + webhook + polling reconciliation, FNE e-invoicing for CI tenants, two React frontends (low-bandwidth resumable customer portal, online-first operator console), all containerized with Docker and deployed via Coolify on Hetzner.

The AWS SaaS Factory gives us the thinking. CLAUDE.md and the V1 PRD give us the constraints. This document is where they meet.


12. Repository layout

Moved here from CLAUDE.md (2026-05-21 shrink). The repository tree is an architecture concern; CLAUDE.md keeps a one-line pointer.

renew-insurance/
├── docs/
│   ├── prd/                    # PRD V0 + V1 + V2/V3 vision + module PRDs (ANALYST). Audit reports under _audit/.
│   ├── architecture/           # Module architectures (ARCHITECT) + this blueprint.
│   ├── design/                 # UX/UI specs (DESIGNER)
│   ├── wireframes/             # /frontend-design prototypes
│   ├── stories/                # Story files (PRODUCT_OWNER)
│   ├── reviews/                # Code reviews (REVIEWER)
│   ├── legal/                  # Compliance memos
│   │   └── audit/              # LEGAL_AUDITOR reports
│   ├── AI_Development_Workflow.md
│   ├── General-Product-Vision.md
│   ├── Considerations-Juridiques-Conformite.md
│   ├── standards/              # Coding + cross-cutting standards (see standards/README.md)
│   └── kb/                     # Personal knowledge base, HUMAN-ONLY, Claude must not read
├── .claude/
│   ├── personalities/          # 7 personality files
│   ├── commands/               # Slash commands
│   ├── plans/                  # Session-to-session handoff plans (Claude read/write)
│   └── worktrees/              # Active worktrees
├── backend/                    # Java 26 / Spring Boot 4 (scaffolded by ARCHITECT in first technical session)
├── frontend/                   # React 19.2 / TypeScript (scaffolded by ARCHITECT in first technical session)
└── CLAUDE.md                   # Project AI context (minimal, points to standards/)

Source code folders (backend/, frontend/) are scaffolded by the ARCHITECT in the first technical session, following the locked stack in docs/standards/tech-stack.md.


Document version: 1.0 - adapted for Papillon Collection Solution V1 Cross-referenced with: CLAUDE.md, docs/prd/prd-v1.md, docs/AI_Development_Workflow.md Next step: ANALYST session per module, using §5 as the canonical module list.