Architecture
SaaS DB vs. Tenant DB
Every starter-template family in this initiative follows the same shape: a template catalog lives in
the central SaaS database (owned by Shumoul.MultiTenancyApi), and an explicit apply step copies
selected rows into one tenant's own database (owned by Shumoul.BackEnd/Shumoul.Api).
Shumoul.MultiTenancyApi never opens a tenant database connection itself.
The Preview → Apply pattern
Every template family exposes the same two-call shape:
POST .../{id}/ApplyToTenant/Preview(or the flow's own equivalent path) — runs full resolution and validation, computes exactly what would be inserted/skipped/updated/blocked, and returns it. Never writes the tenant database. Never writes the central apply log.POST .../{id}/ApplyToTenant— same resolution, but commits. Writes the tenant database (insert-missing, fill-blank-only underForce=true) inside one transaction, then always writes one row to the family's ownTenant*ApplyLogtable in the SaaS DB — whether the outcome was success, partial success, or failure.
The internal apply bridge
Every bridge in this initiative shares the same trust model: a matched pair of internal API keys, one
per bridge purpose (never reused across purposes), checked by hand-written code — never the standard
ASP.NET auth pipeline — against an endpoint marked [AllowAnonymous] and hidden from Swagger
([ApiExplorerSettings(IgnoreApi = true)]).
| Side | Setting | Purpose |
|---|---|---|
| Caller (MultiTenancyApi Host) | AccountingChartTemplateApplyBridgeSettings — Enabled, BackEndBaseUrl, InternalApiKey, one path property per apply flow | Builds the HTTP request, sends X-Shumoul-Internal-Key |
| Receiver (Shumoul.Api) | InternalServiceAuthSettings.AccountingChartTemplateApplyApiKey | Must match the caller's InternalApiKey exactly |
This one key/settings pair is reused, unchanged, by the accounting-accounts, accounting-mappings,
inventory-account-category, cost-center, and (Phase 6/6A.1/6B) all three product-catalog apply flows —
no new secret was introduced for any of them. A missing/wrong key returns 401/403 respectively;
every request also carries an X-Correlation-Id that's threaded through to the central apply log.
This is architecturally distinct from the onboarding apply bridge,
which uses its own separate InternalServiceAuthSettings.OnboardingApplyApiKey, and from the
tenant entitlement provisioning manual-repair
endpoint, which uses yet another separate key (TenantEntitlementProvisioningApiKey) — every internal
bridge in the platform gets its own dedicated key, never a shared one.
Idempotency — no shared mechanism, each layer owns its own
There is no cross-cutting "idempotency service." Every apply flow independently:
- Matches an existing tenant row by a stable key (
Account.Code,CostCenter.Code,ProductCategory.Name,Product.Namescoped to its category,UnitOfMeasure.UnitCode, …). - Skips a match that already exists (never updates a structural field).
- Never resurrects a soft-deleted match — reports it as a failure instead.
- Reports what it did/would do as a list of per-item statuses (
Inserted,SkippedExisting,WouldInsert,Failed,MissingDependency, …) — the exact vocabulary differs slightly per flow (see each flow's own chapter) but the shape is always the same.
The central Tenant*ApplyLog tables are for audit/troubleshooting visibility only — they are never
consulted to decide whether to re-apply. Re-running Apply twice is always safe.
