Customer change requests become DB rows. Not forks.
Every SFA vendor eventually faces the same fork: customer A wants OTP on cash payments, customer B doesn't. Customer C wants a manager-approval step on every invoice over a threshold. Most respond by maintaining per-customer branches. Vendix solves it once: every transaction is an ordered list of steps, and tenants compose the list from a shared catalog.
- 19
- Transaction types
- ∞
- Tenant overrides
- 1
- Codebase
CreateIndirectInvoice · Tenant Demo Pharma
- 1 ValidateOutlet default
- 2 ResolvePriceList default
- 3 CalculateDiscounts default
- 4 EnforceCreditLimit tenant override
- 5 EvaluateFreeIssues default
- 6 AllocateInventoryFEFO default
- 7 CashCollectionOtp tenant override
- 8 PersistInvoice default
- 9 EmitForecastSignal tenant override
3 of 9 steps are tenant-specific. The other 6 ship with the platform.
Tenant CRs without tenant forks.
The traditional way to honor “customer X needs an extra approval step”: a feature flag that
rots, or a code branch that bit-rots faster. Vendix takes a third path -
TenantTransactionSteps
in VendixDb is a per-tenant ordered list of step keys. Want OTP on cash collection?
Insert CashCollectionOtp at position 7.
Want it gone? Delete the row. No deploy needed.
- Default pipeline shared by every tenant - the “happy path” lives in one place.
- Per-tenant overrides via DB rows - insert, replace, or remove steps without code change.
- Step classes deploy globally - once a step exists, every tenant can opt in.
- Web and Android share the pipeline - same steps run regardless of input surface.
Two tenants · same module · different journeys
Tenant A - Pharma
- 1. ValidateOutlet
- 2. ResolvePriceList
- 3. EnforceColdChain
- 4. CalculateDiscounts
- 5. EnforceCreditLimit
- 6. PersistInvoice
Tenant B - FMCG
- 1. ValidateOutlet
- 2. ResolvePriceList
- 3. CalculateDiscounts
- 4. EvaluateFreeIssues
- 5. CashCollectionOtp
- 6. PersistInvoice
Same CreateIndirectInvoiceCommand. Same codebase.
Different rows in TenantTransactionSteps.
A step is just a small reusable rule
Validate outlet
Ships with the platform. Used by every tenant. Checks the outlet exists, is active, and is on the rep's route.
Enforce credit limit
Ships with the platform. Stops invoices that would put the customer over their limit.
Enforce cold chain
Custom step for a pharma tenant. Blocks an invoice if a temperature-sensitive product is in the load and the cold-truck check hasn't been logged.
Cash collection OTP
Custom step for tenants who want one-time-password confirmation on cash payments above a threshold.
Platform steps and custom steps look identical.
Whether a step ships with the platform (validate outlet, enforce credit limit) or was built for a specific tenant (enforce cold chain, cash collection OTP), the engine treats them the same way. Each one runs in order, each one can stop the transaction with a clear reason, and each one is recorded in the audit trail.
- Reusable across transactions - the same "enforce credit limit" step can guard invoices, sales orders, and direct invoices.
- Clear failure reasons - when a step blocks a transaction, the user sees a business-language explanation, not an error code.
- Time-aware - long-running pipelines respect timeouts so a slow step never freezes the rest of the system.
- DI-resolved - steps are registered in
DependencyInjection.csand resolved per request.
When something goes wrong, you can see exactly where.
The pipeline executor records every step run - start, finish, duration, success or failure reason - against a correlation ID that ties it to the originating Mediator command. When a tenant says “the invoice didn't go through” you don't dig through logs; you read the step trace.
- CorrelationIdBehavior stamps every Mediator command, propagated through the whole step chain.
- Per-step result row in the audit trail with field-level diffs (old → new).
- Sensitive props auto-redacted - passwords, tokens, connection strings never hit the audit DB.
Designed to absorb requirement churn.
19 transaction types
PO, GRN, Invoice, Sales Order, Sales Return, Direct Invoice Return, Stock Transfer, Customer Payment, Credit Approval - every transaction is pipelined.
Hot-config by row
Override a step by inserting a row in TenantTransactionSteps. Active on next request - no deploy, no restart.
Web + Android equivalent
Same pipeline runs whether the trigger is a Blazor click, a mobile push, a chatbot tool call, or a system-to-system API.
Source-generator CQRS
Pipeline plays nicely with Mediator v3. Validation, audit, correlation behaviours wrap every step run.
Default pipeline first
New tenants get the platform default. Customisation is opt-in, not opt-out - onboarding is one row.
Idempotent on ClientTxnId
Mobile retries are detected and short-circuited inside the pipeline. No duplicate invoices, no manual reconciliation.
Show us the CR your last vendor refused.
Bring a customisation that previous SFA tools said was a six-month rewrite. We'll wire it as a step in front of you and show you the audit trace before the demo ends.