$ open blueprint
Two strangers agree to work together over the internet.
A client wants the work completed.
A freelancer wants to know the money is actually available.
Neither party completely trusts the other.
The client does not want to release $6,000 before receiving the work.
The freelancer does not want to spend three weeks building something and then discover that payment has become a negotiation.
Meanwhile, the actual relationship is fragmented across:
Email
Slack
Google Drive
Bank Transfer
Payment Gateway
Spreadsheets
Screenshots
The contract lives somewhere else.
The conversation lives somewhere else.
The payment lives somewhere else.
The delivery evidence lives somewhere else.
And when something goes wrong, someone has to reconstruct the relationship manually.
NexusFlow is designed around a different architectural principle:
The conversation, agreement, evidence, and financial state should form one traceable transaction graph.
That does not mean they should share the same database tables or consistency model.
Quite the opposite.
Messaging, contracts, escrow, disputes, and payments are separate domains with different correctness requirements.
They are connected through explicit commands and domain events.
The platform therefore combines:
Real-Time Communication
+
Contract State Management
+
Escrow
+
Double-Entry Accounting
+
Payment Processing
+
Dispute Management
+
Event-Driven Integration
The result is a platform where trust is not assumed.
It is represented by state, enforced by invariants, and supported by an auditable history.
Executive Summary
NexusFlow is a transaction-oriented collaboration platform where communication and financial workflows coexist without sharing the same consistency model.
The architecture separates the system into several bounded domains:
| Domain | Primary Responsibility |
|---|---|
| Identity & Access | Authentication, authorization, tenant isolation, roles |
| User & Organization | Profiles, organizations, memberships, counterparties |
| Messaging | Conversations, messages, delivery, read receipts |
| Presence | Online state, typing indicators, connection state |
| Contracts | Agreement versions, terms, acceptance, lifecycle |
| Milestones | Deliverables, deadlines, submissions, approvals |
| Escrow | Locked funds, allocations, release conditions |
| Wallet | Double-entry financial accounting |
| Payments | Gateway interaction, deposits, payouts, refunds |
| Disputes | Evidence, fund freezing, resolution |
| Notifications | Email, push, in-app notifications |
| Files & Evidence | Deliverables, attachments, immutable references |
| Reporting | Financial and operational read models |
| Audit | Security and business audit history |
| Integrations | Payment providers and external services |
The architecture is built around five fundamental rules:
1. Chat messages are not financial records.
2. Wallet balances are projections of ledger entries.
3. External payment APIs never participate directly
inside database transactions.
4. Domain events are published only after transactional
state has been committed.
5. Every financial operation must be idempotent,
auditable, and reversible through compensating entries.
The Business Problem
Consider a $6,000 software project divided into three milestones:
Milestone 1 → $2,000
Milestone 2 → $2,000
Milestone 3 → $2,000
The client deposits the money.
The freelancer delivers milestone one.
The client approves it.
The platform releases $2,000.
Then milestone two becomes disputed.
At this point, the system must simultaneously answer:
What does the contract say?
Which contract version was accepted?
What exactly was milestone two?
What did the freelancer submit?
When was it submitted?
When did the client receive it?
Was it approved?
Was a dispute opened?
When was the dispute opened?
How much money was allocated to milestone two?
Was that money already released?
Is it currently locked?
What financial entries represent the current state?
Which payment provider transaction corresponds to the deposit?
Which user performed each action?
A simple CRUD application cannot answer these questions reliably.
NexusFlow treats them as architectural concerns.
Architectural Principles
1. Contracts Are Versioned Agreements
A contract is not a mutable text field.
Every material revision creates a new version.
2. Acceptance Is Version-Specific
A user accepts a specific contract version.
Not "the contract."
3. Money Is Ledgered
Balances are derived from financial entries.
They are never the canonical source of truth.
4. Escrow Is a State Machine
Funds move through explicit states:
AVAILABLE
LOCKED
ALLOCATED
DISPUTED
RELEASED
REFUNDED
5. Financial State Is Strongly Consistent
Fund allocation, release, freezing, and refund decisions require transactional guarantees.
6. External Payment Providers Are Untrusted Boundaries
A gateway may:
timeout
retry
duplicate
delay
reorder
The platform must remain correct anyway.
7. WebSockets Are a Delivery Mechanism, Not the Source of Truth
A WebSocket connection can disappear.
Messages must remain durable independently.
8. Redis Is Not the Financial Source of Truth
Redis can accelerate presence, fanout, and cache access.
It must be disposable.
9. Domain Events Represent Facts
Commands express intent.
Events represent completed facts.
10. Read Models Are Rebuildable
Dashboards and projections can be reconstructed from authoritative state and event history.
11. No Cross-Domain Table Mutation
Domains communicate through application services and events, not by directly editing each other's tables.
12. Every Financial Operation Is Traceable
A financial change must have:
Actor
Cause
Reference
Timestamp
Amount
Currency
Ledger Entry
External Reference
where applicable.
High-Level Architecture Blueprint
┌─────────────────────────────┐
│ Client Applications │
│ │
│ Web · Mobile · Admin Portal │
└──────────────┬──────────────┘
│
┌────────────────┴────────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ API Gateway │ │ WebSocket Gateway│
│ │ │ │
│ REST · Auth │ │ Connections │
│ Rate Limits │ │ Presence │
│ Tenant Context │ │ Message Delivery │
└────────┬─────────┘ └────────┬─────────┘
│ │
└────────────────┬───────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Domain Layer │
│ │
│ Identity · Messaging · Contracts │
│ Milestones · Escrow · Wallet │
│ Payments · Disputes · Files │
│ Notifications · Audit │
└──────────────────┬──────────────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ PostgreSQL │ │ Redis │ │ Object Store │
│ │ │ │ │ │
│ Contracts │ │ Presence │ │ Deliverables │
│ Messages │ │ Pub/Sub │ │ Attachments │
│ Ledger │ │ Cache │ │ Evidence │
│ Escrow │ │ Rate Limits │ │ │
│ Outbox │ │ │ │ │
└──────┬──────┘ └──────┬──────┘ └──────────────┘
│ │
│ │
▼ ▼
┌─────────────────────────────────┐
│ Transactional Outbox │
└────────────────┬────────────────┘
│
▼
┌──────────┐
│ Kafka │
│ │
│ Domain │
│ Events │
└────┬─────┘
│
┌───────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌───────────────┐ ┌──────────────┐
│ Read Models │ │ Async Jobs │ │ Integrations │
│ │ │ │ │ │
│ Dashboards │ │ Celery │ │ Payments │
│ Messaging │ │ Notifications │ │ Webhooks │
│ Financial │ │ Reminders │ │ Providers │
└─────────────┘ └───────────────┘ └──────────────┘
Why This Is Not "Just a Chat App With Payments"
The platform contains two fundamentally different categories of state.
Communication State
Message
Typing
Presence
Read Receipt
Connection
Delivery
Failure generally produces:
Latency
Retry
Poor UX
Financial State
Deposit
Escrow Lock
Allocation
Release
Refund
Dispute Freeze
Payout
Failure can produce:
Double spending
Incorrect balances
Lost funds
Incorrect payout
Regulatory exposure
Legal disputes
Therefore the architecture intentionally separates them.
Messaging
│
│ eventual consistency
│
▼
Real-Time Infrastructure
Escrow / Wallet
│
│ strong transactional consistency
│
▼
Financial Ledger
They feel like one product.
They are not one consistency domain.
Domain Architecture
A useful bounded-context structure is:
NexusFlow
│
├── Identity
│
├── Organizations
│
├── Messaging
│
├── Contracts
│
├── Milestones
│
├── Escrow
│
├── Wallet
│
├── Payments
│
├── Disputes
│
├── Files
│
├── Notifications
│
├── Reporting
│
├── Audit
│
└── Integrations
Each domain owns its own business rules.
For example:
Messaging
owns Message
Contracts
owns Contract
Escrow
owns EscrowAccount / FundAllocation
Wallet
owns LedgerEntry
Payments
owns PaymentIntent / ProviderTransaction
Disputes
owns Dispute
Other domains reference these concepts through stable IDs and events.
Command and Event Architecture
NexusFlow distinguishes commands from events.
Commands
A command represents intent.
CreateContract
AcceptContract
SubmitMilestone
ApproveMilestone
OpenDispute
DepositFunds
ReleaseMilestone
RequestRefund
SendMessage
Events
An event represents something that already happened.
contract.created
contract.accepted
milestone.submitted
milestone.approved
dispute.opened
escrow.locked
escrow.released
payment.succeeded
message.sent
The distinction is:
Command
"I want this to happen."
Event
"This happened."
This prevents domain consumers from treating intentions as facts.
Transaction Boundary
A critical architectural rule:
The financial domain owns the transaction that changes financial state.
For example, milestone approval may trigger fund release.
The release operation should be:
Validate
↓
Lock Relevant Rows
↓
Validate Escrow State
↓
Create Ledger Entries
↓
Update Escrow State
↓
Create Outbox Event
↓
Commit
Not:
Update Database
↓
Call Payment Gateway
↓
Wait
↓
Update Database
External payment APIs cannot safely participate in the PostgreSQL transaction.
Double-Entry Wallet Architecture
A serious financial system should not rely on:
wallet.balance += 2000
as its source of truth.
Instead, use a double-entry ledger.
Conceptually:
Ledger Account
│
├── Debit
└── Credit
Every financial transaction must balance.
For example, when client funds enter platform-controlled escrow:
Client Funding Account
Credit +6000
Escrow Liability Account
Debit -6000
The exact debit/credit orientation depends on the accounting model, but the invariant remains:
Total Debits = Total Credits
The important principle is that money is represented as movements between accounts, not simply as changes to a balance column.
Wallet Account Model
A user may have multiple financial accounts.
User
│
├── Available Wallet
│
├── Pending Wallet
│
└── Escrow-related Accounts
At the platform level:
Platform
│
├── Customer Funds
├── Escrow Liabilities
├── Freelancer Payables
├── Fees
└── Settlement Accounts
This allows the system to distinguish:
Money owned by user
vs
Money currently locked
vs
Money owed to user
vs
Platform revenue
That distinction becomes extremely important as the platform grows.
Ledger Entry Model
A simplified conceptual model:
LedgerEntry(
transaction_id,
account_id,
entry_type,
amount,
currency,
reference_type,
reference_id,
occurred_at,
)
A financial transaction contains multiple entries.
Example:
Transaction: ESCROW_LOCK
Client Escrow Source
-2000
Milestone Escrow Liability
+2000
Another:
Transaction: MILESTONE_RELEASE
Milestone Escrow Liability
-2000
Freelancer Payable
+2000
Then:
Transaction: PAYOUT_SETTLED
Freelancer Payable
-2000
External Settlement Account
+2000
The platform can reconstruct the financial state from these movements.
Balance Is a Projection
A balance can still exist for performance.
For example:
wallet_balance_projection
might contain:
account_id
currency
available_balance
pending_balance
updated_at
version
But this is a projection.
The authoritative history remains the ledger.
If the projection becomes corrupted:
Ledger
│
▼
Replay / Recalculate
│
▼
Balance Projection
This is a critical architectural distinction.
Currency and Money Representation
Financial amounts should never be represented using floating-point arithmetic.
Avoid:
amount = 0.1 + 0.2
Use fixed-precision decimal or integer minor units.
For example:
USD 20.50
can be represented as:
2050 cents
The system must also make currency explicit:
amount
currency
A ledger entry without currency is incomplete.
Escrow Domain
Escrow represents the business state of locked funds associated with contractual obligations.
A simplified model:
Contract
│
└── Escrow Agreement
│
├── Milestone 1
├── Milestone 2
└── Milestone 3
Each milestone may have:
amount
currency
release_condition
deadline
status
Escrow State Machine
A realistic lifecycle:
CREATED
│
▼
FUNDED
│
▼
ALLOCATED
│
├───────────────┐
│ │
▼ ▼
DELIVERED DISPUTED
│ │
▼ │
UNDER_REVIEW │
│ │
├───────┬───────┘
│ │
▼ ▼
APPROVED RESOLVED
│ │
▼ ├── RELEASED
RELEASED │
├── REFUNDED
│
└── SPLIT
The exact state machine can vary, but the important property is:
Invalid transitions must be rejected by the domain.
Escrow Allocation
A deposit and a milestone allocation are not necessarily the same operation.
Example:
Client deposits $6,000
│
▼
Platform-controlled escrow
│
├── Milestone 1 → $2,000
├── Milestone 2 → $2,000
└── Milestone 3 → $2,000
The platform must know:
total funded
total allocated
total released
total disputed
total refunded
remaining
with accounting invariants preventing impossible states.
Escrow Invariants
Examples:
Allocated amount
cannot exceed funded amount.
Released amount
cannot exceed allocated amount.
Refunded amount
cannot exceed refundable amount.
Disputed funds
cannot be released while dispute is active.
A completed release
cannot be executed twice.
A cancelled milestone
cannot release funds without an explicit resolution.
A ledger transaction
must balance.
Currency must match
across an escrow transaction.
These are not merely application conventions.
They are domain invariants.
Milestone Architecture
A milestone represents a contractual obligation.
Milestone
│
├── Title
├── Description
├── Amount
├── Due Date
├── Acceptance Criteria
├── Status
├── Contract Version
└── Delivery
A milestone should not be considered complete merely because someone clicked:
completed = true
Instead:
Freelancer
│
▼
Submit Delivery
│
▼
Delivery Recorded
│
▼
Client Review
│
├── Approve
├── Request Revision
└── Dispute
Delivery Architecture
A delivery may include:
Files
Links
Commit References
Notes
Screenshots
Build Artifacts
Files themselves should live in object storage.
The database stores:
file_id
object_key
checksum
content_type
size
uploaded_by
created_at
This avoids placing large binary objects inside PostgreSQL.
File Integrity
For important deliverables, the platform can store:
SHA-256 checksum
for each file.
Example:
delivery_8821.zip
checksum:
8f3a...
If the file changes, the checksum changes.
This provides a stronger evidence trail than simply storing a filename.
Contract Architecture
A contract should contain immutable versions.
Contract
│
├── Version 1
├── Version 2
└── Version 3
Version 3 may be the active version.
But Versions 1 and 2 remain historical.
Contract Version Lifecycle
DRAFT
│
▼
PROPOSED
│
▼
NEGOTIATION
│
├───────────────┐
│ │
▼ │
REVISION_REQUESTED
│
└───────────────┘
│
▼
READY_FOR_ACCEPTANCE
│
▼
PARTIALLY_ACCEPTED
│
▼
FULLY_ACCEPTED
│
▼
ACTIVE
│
▼
COMPLETED
A contract may also enter:
CANCELLED
TERMINATED
DISPUTED
depending on the business model.
Contract Acceptance
Acceptance must bind the user to a specific version.
Conceptually:
Contract Version 7
│
├── Client accepted
│ timestamp
│ user_id
│
└── Freelancer accepted
timestamp
user_id
If Version 8 is later created:
Version 7
→ previously accepted
Version 8
→ requires new acceptance
This avoids ambiguity about which terms governed a transaction.
Contract Integrity
For legally sensitive workflows, the platform can generate a canonical representation of the accepted contract version and store:
content_hash
version_number
accepted_at
accepted_by
The exact legal enforceability depends on jurisdiction and implementation, but technically this creates a verifiable record of what was accepted.
Contract Changes After Funding
One of the harder business cases is:
Contract Active
│
▼
Funds Already Locked
│
▼
Terms Changed
The system should not silently mutate the existing contract.
Instead:
Current Contract
│
▼
Amendment Proposed
│
▼
New Contract Version
│
▼
Both Parties Accept
│
▼
New Terms Become Effective
Financial consequences should be explicit.
Real-Time Messaging Architecture
The messaging system is optimized for:
Low latency
High concurrency
Connection resilience
Durability
Ordering within conversations
Architecture:
Client
│
▼
WebSocket Gateway
│
├── Authenticate
├── Authorize
└── Validate Command
│
▼
Messaging Domain
│
▼
PostgreSQL
│
▼
Message Persisted
│
▼
Redis Pub/Sub
│
▼
WebSocket Fanout
The WebSocket is therefore the delivery path.
PostgreSQL remains the durable message history.
WebSocket Authentication
The connection lifecycle should be:
Client
│
▼
Open WebSocket
│
▼
Authenticate
│
▼
Resolve User
│
▼
Resolve Organization / Workspace
│
▼
Authorize Conversation
│
▼
Connection Accepted
Authentication is not authorization.
A valid user does not automatically have access to every conversation.
WebSocket Connection Registry
A gateway may maintain ephemeral state:
user_id
connection_id
server_id
last_heartbeat
subscriptions
Redis can maintain this information.
Example:
user:123
connections:
gateway-01
gateway-04
This allows a user to be connected from multiple devices.
Presence Architecture
Presence is intentionally ephemeral.
WebSocket Connected
│
▼
SET presence:user_id
TTL 30 seconds
│
▼
Heartbeat every 10–15 seconds
│
▼
TTL refreshed
If the client disappears:
Heartbeat stops
│
▼
TTL expires
│
▼
Offline
No database transaction is required.
Typing Indicators
Typing is even more ephemeral.
It should generally not be persisted.
typing.started
typing.stopped
can be sent through Redis Pub/Sub.
If the event is lost:
Nothing financially important happened.
This is exactly the kind of workload Redis Pub/Sub is appropriate for.
Message Persistence
Messages are durable.
A message operation should conceptually be:
SendMessage Command
│
▼
Authorization
│
▼
Idempotency Check
│
▼
Persist Message
│
▼
Create Outbox Event
│
▼
Commit
│
▼
Publish
│
▼
WebSocket Fanout
This prevents a message from being broadcast as successful before it is durable.
Message Idempotency
Mobile networks are unreliable.
A client may send:
"Here is the final build."
and then receive no response.
The client retries.
Without idempotency:
Message A
Message A duplicate
With:
client_message_id
the server can recognize the retry.
Example:
client_message_id:
01JX8K7...
Database uniqueness ensures one logical message produces one durable record.
Message Ordering
Global ordering is unnecessary.
Conversation-level ordering matters more.
A message can contain:
conversation_id
sequence_number
created_at
message_id
A per-conversation sequence can provide deterministic ordering where required.
Distributed WebSocket delivery should not rely exclusively on arrival time.
Delivery Receipts
A message can have:
SENT
DELIVERED
READ
But these are not the same as message existence.
The durable message record remains:
Message
Delivery state can be modeled separately:
MessageDelivery
message_id
user_id
delivered_at
read_at
This prevents one recipient's read state from mutating the message itself.
Messaging and Financial Events
Messaging should not directly release funds.
For example:
Message:
"Client approved milestone 2."
must not itself cause:
Release Funds
Instead, approval must be an explicit domain command:
ApproveMilestone
which creates:
milestone.approved
The Escrow domain reacts according to its own rules.
This prevents conversational content from becoming an implicit financial command.
Event Backbone
For business events, NexusFlow should use a durable event backbone.
PostgreSQL
│
▼
Outbox
│
▼
Kafka
│
├── Messaging Projection
├── Contract Projection
├── Escrow Projection
├── Notification Service
├── Reporting
└── Audit
Redis Pub/Sub remains appropriate for ephemeral real-time fanout.
This creates an important distinction:
Redis Pub/Sub
→ ephemeral delivery
Kafka
→ durable domain-event distribution
Event Envelope
A standard event envelope might contain:
{
"event_id": "evt_01JX...",
"event_type": "milestone.approved",
"event_version": 1,
"occurred_at": "2026-08-18T12:30:00Z",
"organization_id": "org_123",
"aggregate_type": "milestone",
"aggregate_id": "milestone_8821",
"correlation_id": "contract_991",
"causation_id": "cmd_8821",
"actor_id": "user_123",
"payload": {
"contract_id": "contract_100",
"milestone_id": "milestone_8821"
}
}
This metadata enables distributed tracing and auditing.
Event Versioning
Events are contracts.
They should be versioned.
milestone.approved.v1
milestone.approved.v2
A consumer should not silently break because the producer added or changed fields.
Schema compatibility should be part of event governance.
Kafka Partitioning
Events should be partitioned based on ordering requirements.
For contract-related events:
partition_key =
organization_id + contract_id
This allows events for one contract to preserve relative ordering while unrelated contracts process concurrently.
For messaging projections:
partition_key =
conversation_id
may be appropriate.
Event Idempotency
Consumers should assume duplicate delivery.
A consumer may maintain:
ProcessedEvent
-------------------------
consumer_name
event_id
processed_at
Before processing:
if event already processed:
return
The event processing and idempotency record should be handled transactionally where the consumer's storage model permits it.
Payment Architecture
Payments are an external distributed system boundary.
A payment provider may return:
success
timeout
pending
failure
duplicate webhook
late webhook
Therefore:
Payment Request
│
▼
Payment Intent
│
▼
External Provider
│
▼
Webhook
│
▼
Webhook Verification
│
▼
Idempotent Processing
│
▼
Financial Transaction
The webhook, not a browser redirect, should generally be treated as the authoritative signal for asynchronous payment completion.
Payment Intent
The platform should create an internal payment intent before calling the provider.
Example:
PaymentIntent
----------------
id
organization_id
user_id
amount
currency
purpose
status
provider
provider_reference
idempotency_key
created_at
Possible states:
CREATED
PROCESSING
SUCCEEDED
FAILED
CANCELLED
EXPIRED
Payment Webhook Security
External payment webhooks must be verified.
The processing pipeline should be:
Webhook Received
│
▼
Verify Signature
│
▼
Validate Timestamp / Replay Rules
│
▼
Check Provider Event ID
│
▼
Load Payment Intent
│
▼
Validate Expected State
│
▼
Apply Financial Transaction
│
▼
Record Webhook
│
▼
Create Domain Event
The system must not trust:
amount
status
user_id
from an unverified client-side request.
Payment Idempotency
A client should never be able to cause multiple financial effects by retrying the same request.
Use:
Idempotency-Key
for payment creation.
The provider should also receive a provider-supported idempotency key where available.
There should be multiple protection layers:
Client
↓
NexusFlow
↓
Payment Provider
Deposit Flow
A realistic deposit flow:
Client Funds Contract
│
▼
Create Payment Intent
│
▼
Payment Provider
│
▼
Payment Succeeds
│
▼
Verified Webhook
│
▼
Financial Transaction
│
├── External Funds Receivable
├── Escrow Liability
└── Payment Record
│
▼
Outbox Event
│
▼
funds.deposited
The browser returning to:
/payment/success
is not sufficient proof that money arrived.
Escrow Release Architecture
The release flow must be transactionally safe.
Client Approves Milestone
│
▼
ApproveMilestone Command
│
▼
Milestone Transaction
│
├── Lock Milestone Row
├── Validate Status
├── Validate Escrow State
├── Create Ledger Entries
├── Update Escrow State
└── Create Outbox Event
│
▼
COMMIT
│
▼
escrow.release_authorized
The actual external payout may happen asynchronously.
Why Payout Must Not Happen Inside the Database Transaction
Avoid:
with transaction.atomic():
release_funds()
payment_provider.send_payout()
The external API may:
accept the payout
and then:
timeout before returning a response
The application cannot know whether the external action succeeded.
If the database transaction rolls back and the application retries, it may create a duplicate payout.
Instead:
Financial Transaction
│
▼
Payout Instruction
│
▼
Outbox
│
▼
Payment Worker
│
▼
Provider API
│
▼
Provider Webhook / Status Query
│
▼
Settlement Confirmation
The payout process becomes an explicitly tracked state machine.
Payout State Machine
AUTHORIZED
│
▼
SUBMITTED
│
├──────────────┐
│ │
▼ ▼
PROCESSING FAILED
│
▼
SETTLED
If the provider times out:
SUBMITTED
│
▼
UNKNOWN
│
▼
Reconciliation
│
├── Settled
└── Failed
The platform must not simply retry blindly when the external state is unknown.
Dispute Architecture
A dispute is a financial control mechanism.
Opening a dispute should synchronously change the relevant escrow state.
OpenDispute
│
▼
Validate Eligibility
│
▼
Lock Escrow / Milestone
│
▼
Set DISPUTED
│
▼
Create Evidence Case
│
▼
Create Outbox Event
│
▼
Notify Parties
The financial freeze occurs inside the transaction.
Notifications happen asynchronously.
Dispute State Machine
OPENED
│
▼
EVIDENCE_COLLECTION
│
▼
UNDER_REVIEW
│
├──────────────┬───────────────┐
▼ ▼ ▼
RELEASE REFUND SPLIT
│ │ │
└──────────────┴───────────────┘
│
▼
CLOSED
A dispute should never directly modify ledger history.
Resolution creates new financial transactions.
Dispute Resolution as a Compensating Transaction
Suppose:
Milestone = $2,000
and the dispute resolves:
$1,500 → Freelancer
$500 → Client
The system should create new ledger entries representing the resolution.
It should not rewrite:
original escrow transaction
Financial history remains immutable.
Evidence Architecture
Evidence can include:
Contract Version
Messages
Message Attachments
Delivery Files
Delivery Timestamp
Milestone Submission
Approval
Revision Requests
Dispute Comments
Payment Records
The dispute domain should reference these records rather than copying entire datasets.
This creates a connected evidence graph:
Dispute
│
├── Contract Version
├── Milestone
├── Delivery
├── Messages
├── Files
└── Financial Transactions
Evidence Immutability
Evidence should be append-oriented.
A message should not silently change after a dispute is opened.
If an administrative correction is required:
Original Record
│
▼
Correction Event
rather than:
UPDATE original_history
The exact legal retention requirements depend on jurisdiction and business model, but technically the architecture should preserve historical evidence.
Notifications Architecture
Notifications should not block financial operations.
For example:
escrow.released
│
├── Email
├── Push
├── In-App
└── WebSocket
Each notification channel can process independently.
If email fails:
Escrow release remains successful.
Real-Time Financial Updates
When a milestone is approved:
Financial Transaction
│
▼
Domain Event
│
├── Reporting
├── Notifications
└── Real-Time UI Projection
The UI may immediately receive:
milestone.approved
escrow.release_authorized
But the UI should not assume an external payout has settled merely because the milestone was approved.
This distinction prevents misleading financial UX.
CQRS Architecture
Operational writes and complex reads have different requirements.
Write model:
Contracts
Milestones
Ledger
Escrow
Disputes
Payments
Read model:
Conversation Inbox
Contract Dashboard
Escrow Dashboard
Wallet Balance
Dispute Timeline
Transaction History
Admin Reporting
Architecture:
PostgreSQL
Transactional Store
│
▼
Domain Events
│
▼
Kafka
│
┌─────────┼──────────┐
▼ ▼ ▼
Messaging Finance Contract
Projection Projection Projection
│ │ │
└─────────┼──────────┘
▼
Query APIs
Financial Read Models
A dashboard might need:
Total Funded
Total Locked
Total Released
Total Pending
Total Disputed
Total Refunded
These should not require scanning millions of ledger entries for every request.
A projection can maintain these aggregates.
But:
Financial Projection
is not:
Financial Truth
If there is ever a discrepancy:
Ledger wins.
Conversation Read Models
The inbox may need:
Last Message
Unread Count
Last Activity
Participant Name
Contract ID
Milestone Status
This can be represented by a denormalized read model.
The underlying messages remain normalized and durable.
Read Model Rebuilding
If a projection is corrupted:
Authoritative State
│
▼
Replay Events / Recalculate
│
▼
New Projection
This is particularly useful for:
financial dashboards
contract timelines
analytics
notification state
Multi-Tenant Architecture
NexusFlow should support:
Organization
│
├── Users
├── Teams
├── Contracts
├── Conversations
├── Wallet Accounts
├── Escrow Agreements
└── Disputes
Every tenant-owned object should be associated with:
organization_id
where appropriate.
Tenant context should be established at the edge of every authenticated request.
Tenant Isolation
Tenant isolation should use defense in depth:
Authentication
│
▼
Tenant Resolution
│
▼
Authorization
│
▼
Application-Level Filtering
│
▼
Database Constraints / RLS where appropriate
A frontend route such as:
/contracts/123
must never be considered sufficient authorization.
The backend must verify:
Contract 123
belongs to
Organization X
and
User Y has access to it.
Contract Access Model
A contract may involve:
Client
Freelancer
Organization
Admin
Dispute Reviewer
The permission model should distinguish:
View Contract
Edit Draft
Submit Revision
Accept Version
Submit Milestone
Approve Milestone
Open Dispute
Resolve Dispute
Release Funds
These should not collapse into a single:
is_member = true
permission.
Security Architecture
The security boundary should look like:
Internet
│
▼
TLS
│
▼
API / WebSocket Gateway
│
▼
Authentication
│
▼
Tenant Context
│
▼
Authorization
│
▼
Domain Command
│
▼
Business Invariants
│
▼
Transactional State
Financial actions should require stronger authorization than ordinary messaging actions.
Rate Limiting
Different operations require different limits.
Message Send
→ high rate
Contract Creation
→ moderate rate
Dispute Opening
→ low rate
Payment Creation
→ strict rate
Payout Request
→ very strict rate
Rate limiting can be implemented at multiple layers:
API Gateway
+
Redis
+
Domain-Level Rules
Concurrency Control
Financial state is highly concurrent.
Suppose two requests arrive simultaneously:
Request A → Release $2,000
Request B → Release $2,000
Both see:
status = LOCKED
if there is no proper locking.
The result could be a double release.
Use:
with transaction.atomic():
escrow = (
EscrowAllocation.objects
.select_for_update()
.get(id=allocation_id)
)
if escrow.status != "LOCKED":
raise InvalidStateTransition()
escrow.status = "RELEASE_AUTHORIZED"
escrow.save()
create_ledger_transaction(...)
create_outbox_event(...)
The second transaction waits and then sees the updated state.
Optimistic Concurrency
For some non-financial resources, version numbers may be sufficient.
Example:
Contract Version
version = 7
Client updates:
expected_version = 7
If the actual version is already:
8
the operation fails with:
Conflict
This prevents silent overwrites.
Financial Invariants
The system should continuously enforce invariants such as:
Ledger debits = ledger credits.
Released funds
cannot exceed allocated funds.
Allocated funds
cannot exceed funded funds.
Disputed funds
cannot be released.
Settled payout
cannot return to pending.
A payment webhook
cannot create duplicate financial effects.
A refund
cannot exceed refundable funds.
A transaction
cannot mix currencies without explicit conversion logic.
An escrow allocation
must belong to the correct contract and organization.
These rules should exist in the domain model and, where practical, be reinforced by database constraints.
Reconciliation Architecture
A production financial platform must assume that external state can diverge.
NexusFlow should run reconciliation processes.
Examples:
Internal Payment Intent
vs
Provider Payment Status
Internal Payout
vs
Provider Payout Status
Ledger
vs
Wallet Projection
Escrow State
vs
Milestone State
Payment Reconciliation
Example:
Payout Status = PROCESSING
Provider says = SETTLED
The reconciliation worker detects the difference.
Provider API / Webhook
│
▼
Reconciliation Worker
│
▼
Validate External Reference
│
▼
Apply Settlement Transaction
│
▼
Emit payout.settled
This is safer than relying entirely on one webhook.
Unknown External State
One of the hardest distributed-system cases:
Application sends payout
│
▼
Provider accepts request
│
▼
Network timeout
The platform does not know:
Succeeded?
Failed?
Processing?
The correct state is not necessarily:
FAILED
It may be:
UNKNOWN
The system then reconciles using:
provider_reference
before attempting any potentially duplicative operation.
Failure Handling
NexusFlow assumes:
WebSocket disconnects
Redis disappears
Kafka delays
Database transaction fails
Worker crashes
Payment provider times out
Webhook duplicates
Webhook arrives late
Email fails
Mobile network retries
The architecture responds with:
Transactions
+
Idempotency
+
Outbox
+
Retries
+
Dead Letter Queues
+
Reconciliation
+
Observability
Dead Letter Queue
Repeated event-processing failures should not block an entire stream.
Kafka
│
▼
Consumer
│
├── Success
│
└── Failure
│
▼
Retry
│
├── Success
│
└── Repeated Failure
│
▼
DLQ
DLQ messages should retain:
event_id
event_type
payload
consumer
failure_reason
retry_count
first_failed_at
last_failed_at
Outbox Architecture
Any transactional domain change that needs to produce an event should use:
Business State
+
Outbox Event
inside the same database transaction.
Example:
with transaction.atomic():
milestone.approve()
create_ledger_transaction()
OutboxEvent.objects.create(
event_type="milestone.approved",
aggregate_id=milestone.id,
payload=payload,
)
Then:
Outbox Publisher
│
▼
Kafka
This prevents the classic dual-write failure:
Database succeeds
Kafka fails
or:
Kafka succeeds
Database fails
Audit Architecture
Audit is separate from business ledgering.
Financial ledger answers:
What happened to money?
Business audit answers:
Who performed what action?
Security audit answers:
Who changed permissions or security state?
Examples:
Contract accepted
Role changed
Dispute opened
Admin viewed evidence
Payout initiated
Payment method changed
Each can carry:
actor_id
organization_id
timestamp
IP / device metadata where appropriate
correlation_id
resource
action
result
Sensitive information should not be indiscriminately copied into logs.
Observability Architecture
The system should expose both technical and business telemetry.
Technical Metrics
WebSocket connections
Messages/sec
WebSocket reconnect rate
Redis latency
Kafka consumer lag
Outbox backlog
Worker queue depth
Database latency
Payment API latency
Webhook processing failures
DLQ depth
Business Metrics
Contracts created
Contracts accepted
Milestones submitted
Milestones approved
Disputes opened
Funds locked
Funds released
Funds refunded
Payouts pending
Payout failures
Distributed Tracing
A single financial operation should be traceable:
Approve Milestone
│
▼
API Request
│
▼
Escrow Transaction
│
▼
Ledger Entry
│
▼
Outbox
│
▼
Kafka
│
▼
Notification
│
▼
Payout Worker
│
▼
Payment Provider
A shared:
correlation_id
allows operators to follow the entire lifecycle.
Data Architecture
A simplified transactional database structure:
Identity
├── User
├── Organization
└── Membership
Messaging
├── Conversation
├── ConversationMember
├── Message
└── MessageDelivery
Contracts
├── Contract
├── ContractVersion
├── ContractParticipant
└── ContractAcceptance
Milestones
├── Milestone
├── MilestoneDelivery
└── MilestoneReview
Escrow
├── EscrowAgreement
├── EscrowAllocation
└── EscrowStateHistory
Wallet
├── Account
├── LedgerTransaction
└── LedgerEntry
Payments
├── PaymentIntent
├── ProviderTransaction
├── Payout
└── PaymentWebhook
Disputes
├── Dispute
├── DisputeParticipant
├── EvidenceReference
└── Resolution
Infrastructure
├── OutboxEvent
├── IdempotencyRecord
├── ProcessedEvent
└── AuditLog
Why the Wallet Should Not Be a Single Table With Balance
A simplistic design:
Wallet
------
user_id
balance
cannot explain:
Where did the balance come from?
A ledger can.
LedgerTransaction
│
├── Entry A
├── Entry B
└── Entry C
A wallet balance becomes:
SUM(credits) - SUM(debits)
or, for a true double-entry system, the appropriate account-side balance derived from all entries.
A cached balance can exist for speed.
The ledger remains authoritative.
Storage Strategy
PostgreSQL
Use for:
Contracts
Messages
Milestones
Escrow
Wallet Ledger
Payments
Disputes
Outbox
Audit Metadata
Redis
Use for:
Presence
Typing
WebSocket fanout
Rate limits
Short-lived cache
Connection registry
Kafka
Use for:
Durable domain event distribution
Object Storage
Use for:
Contract documents
Milestone files
Attachments
Dispute evidence
Exports
Read Store
Depending on scale:
PostgreSQL read models
or
dedicated analytical/query database
The choice should follow actual query volume.
Modular Monolith Strategy
The first production implementation does not need to be dozens of microservices.
FastAPI can host a modular architecture:
nexusflow/
├── identity/
├── organizations/
├── messaging/
├── contracts/
├── milestones/
├── escrow/
├── wallet/
├── payments/
├── disputes/
├── files/
├── notifications/
├── reporting/
├── audit/
└── integrations/
Each module should expose explicit application services.
For example:
escrow.services.release_milestone()
payments.services.create_payment_intent()
contracts.services.accept_contract()
messaging.services.send_message()
disputes.services.open_dispute()
Modules should not randomly import and mutate each other's persistence layer.
When to Extract Services
Not every domain needs its own deployment.
Potential future extraction candidates:
Messaging Gateway
Notification Service
Payment Integration
File Processing
Reporting
Forecasting / Analytics
The wallet and escrow domains should be extracted only when there is a strong operational reason.
They are consistency-sensitive.
A distributed financial architecture introduces additional failure modes.
Deployment Architecture
A production deployment could evolve toward:
Internet
│
▼
Load Balancer
│
┌────────────┴────────────┐
▼ ▼
API Instances WebSocket
FastAPI x N Gateway x N
│ │
└────────────┬────────────┘
│
┌────────┴────────┐
▼ ▼
PostgreSQL Redis
│ │
▼ ▼
Outbox Presence/Fanout
│
▼
Kafka
│
┌───────┼────────┐
▼ ▼ ▼
Workers Read Integrations
Models
Scaling WebSockets
WebSocket connections are stateful at the connection level.
If:
Gateway A
holds:
User 1
User 2
User 3
and:
Gateway B
holds:
User 4
User 5
a message from User 1 to User 5 must cross gateway boundaries.
Redis Pub/Sub can provide fanout:
Gateway A
│
▼
Redis Channel
│
▼
Gateway B
│
▼
User 5
For ephemeral real-time delivery this is appropriate.
Durable message history remains in PostgreSQL.
WebSocket Reconnection
Clients should assume connections disappear.
On reconnect:
Reconnect
│
▼
Authenticate
│
▼
Restore Subscriptions
│
▼
Fetch Messages Since Last Known Position
│
▼
Resume Real-Time Delivery
This is more reliable than assuming every WebSocket event was received.
A client may maintain:
last_seen_message_sequence
and synchronize missed messages after reconnect.
Backpressure
A slow client should not be able to consume unlimited memory on the gateway.
The system should define:
maximum queued messages
maximum frame size
heartbeat timeout
connection timeout
rate limits
If a client cannot keep up:
Queue grows
│
▼
Threshold reached
│
▼
Disconnect / Resynchronize
The client can reconnect and fetch durable messages from the database.
Security for WebSockets
WebSocket authorization should be checked at subscription time.
Example:
User connects
│
▼
Authenticated
│
▼
Subscribe conversation: 8842
│
▼
Check membership
│
├── Allowed
└── Denied
A valid JWT alone should not grant access to arbitrary conversation channels.
Contract and Message Relationship
Messages should not become contract terms merely because they exist in a conversation.
Instead:
Conversation
│
▼
Contract Proposal
│
▼
Contract Version
│
▼
Acceptance
This creates a clear boundary between:
informal communication
and:
governed contractual state
A message can reference a contract or milestone.
It should not silently mutate it.
Dispute Evidence Graph
When a dispute opens:
Dispute
│
├── Contract Version
│
├── Milestone
│
├── Delivery
│
├── Messages
│
├── Files
│
├── Approval Events
│
└── Financial Ledger Transactions
This allows a reviewer to reconstruct the entire transaction.
End-to-End Contract Creation
Client Creates Draft
│
▼
Contract Version 1
│
▼
Freelancer Reviews
│
├── Accept
│
└── Request Revision
│
▼
Contract Version 2
│
▼
Both Parties Accept
│
▼
Contract Active
│
▼
Milestones Activated
End-to-End Funding
Client Funds Contract
│
▼
Payment Intent Created
│
▼
Provider Checkout
│
▼
Provider Confirms Payment
│
▼
Verified Webhook
│
▼
Payment Transaction
│
▼
Double-Entry Ledger
│
▼
Escrow Funded
│
▼
funds.deposited
│
├── Notification
├── Dashboard
└── Audit
End-to-End Milestone Approval
Freelancer
│
▼
Submit Milestone
│
▼
Delivery Recorded
│
▼
Client Reviews
│
▼
Approve
│
▼
Lock Milestone Row
│
▼
Validate Escrow
│
▼
Create Ledger Transaction
│
▼
Escrow Release Authorized
│
▼
Commit
│
▼
Outbox
│
▼
Kafka
│
├── Notification
├── Reporting
└── Payout Worker
End-to-End Dispute
Client Opens Dispute
│
▼
Validate Milestone
│
▼
Transaction
│
├── Escrow → DISPUTED
├── Release Blocked
├── Dispute Created
└── Outbox Event
│
▼
Commit
│
▼
Evidence Collection
│
▼
Administrative Review
│
▼
Resolution
│
├── Release
├── Refund
└── Split
│
▼
Ledger Transaction
│
▼
Dispute Closed
End-to-End Payout
Milestone Release Authorized
│
▼
Payout Instruction Created
│
▼
Outbox
│
▼
Payout Worker
│
▼
Payment Provider
│
├── Accepted
│
▼
Payout = PROCESSING
│
▼
Provider Webhook
│
▼
Verify + Idempotency
│
▼
Payout = SETTLED
│
▼
Settlement Ledger Transaction
│
▼
payout.settled
The internal ledger therefore tracks not only:
"we want to pay"
but:
authorized
submitted
processing
settled
which reflects the actual lifecycle of an external financial operation.
Failure Scenario: Payment Webhook Arrives Twice
Webhook #77881
│
▼
Process
│
▼
Financial Transaction
│
▼
Processed
Then:
Webhook #77881
│
▼
Already Processed
│
▼
No-op
No second ledger transaction is created.
Failure Scenario: Client Approves Twice
Approval Request A
Approval Request B
│
▼
Same Milestone
│
▼
select_for_update()
│
▼
Request A → RELEASE_AUTHORIZED
│
▼
Request B
│
▼
State is no longer releasable
│
▼
Rejected / Idempotent Result
No double release.
Failure Scenario: WebSocket Dies After Send
Client
│
▼
Send Message
│
▼
Server persists message
│
▼
Connection drops
The sender may not receive confirmation.
It retries using:
client_message_id
The server finds the existing message.
Result:
One durable message
not two.
Failure Scenario: Redis Goes Down
Presence may temporarily become unavailable.
Typing indicators may disappear.
Real-time fanout may degrade.
But:
Messages
Contracts
Escrow
Ledger
Payments
Disputes
remain durable in PostgreSQL.
The architecture therefore degrades without losing financial truth.
Failure Scenario: Kafka Goes Down
Business transactions continue writing:
PostgreSQL
+
Outbox
Events accumulate in the outbox.
When Kafka recovers:
Outbox
│
▼
Kafka
publishing resumes.
The warehouse of events is delayed.
The financial truth is not lost.
Failure Scenario: Payment Provider Goes Down
The platform can still:
Create internal payment intent
but must not claim:
Funds received
until provider confirmation exists.
The payment remains:
PENDING
rather than being treated as successful based on optimistic assumptions.
Failure Scenario: Provider Times Out After Accepting Payout
This is the dangerous case:
NexusFlow
│
▼
Provider
│
▼
Accepted
│
▼
Network Timeout
The platform records:
Payout = UNKNOWN
and reconciles using:
provider_reference
before retrying.
This prevents accidental duplicate payouts.
Reconciliation Jobs
Scheduled jobs should verify:
Payment Intent ↔ Provider
Payout ↔ Provider
Ledger ↔ Balance Projection
Escrow ↔ Milestone
Contract ↔ Acceptance
Dispute ↔ Escrow State
Reconciliation is not an admission of architectural failure.
It is a normal defense against distributed-system uncertainty.
Data Retention
Different data classes may require different retention policies.
Financial Ledger
→ long-term retention
Contract Versions
→ long-term retention
Dispute Evidence
→ policy-driven retention
Messages
→ product/legal retention policy
Presence
→ ephemeral
Typing
→ ephemeral
Cache
→ disposable
Retention requirements should ultimately be driven by product, legal, and regulatory requirements applicable to the deployment.
Architecture Quality Attributes
| Attribute | Architectural Mechanism |
|---|---|
| Financial Correctness | Double-entry ledger + transactions |
| Escrow Safety | State machine + row locking |
| Payment Reliability | Idempotency + webhook verification |
| Real-Time Performance | WebSockets + Redis fanout |
| Message Durability | PostgreSQL persistence |
| Contract Integrity | Immutable versioning + acceptance |
| Dispute Integrity | Automatic fund freeze + evidence graph |
| Event Reliability | Transactional Outbox |
| Distributed Resilience | Retry + DLQ + reconciliation |
| Scalability | CQRS + asynchronous workers |
| Tenant Isolation | Organization scope + authorization |
| Auditability | Ledger + audit trail + correlation IDs |
| Recoverability | Backups + replayable projections |
| Extensibility | Domain events + modular boundaries |
| Maintainability | Modular monolith + explicit contracts |
Source of Truth Matrix
| Data | Authoritative Source |
|---|---|
| Messages | PostgreSQL |
| Presence | Redis |
| Typing State | Redis |
| Contract Versions | PostgreSQL |
| Contract Acceptance | PostgreSQL |
| Milestone State | PostgreSQL |
| Escrow State | PostgreSQL |
| Financial Ledger | PostgreSQL |
| Wallet Balance | Derived from Ledger |
| Payment Intent | PostgreSQL |
| Provider Payment State | External Provider + Verified Internal Record |
| Payout State | Internal Payout State + Provider Confirmation |
| Dispute | PostgreSQL |
| Evidence Files | Object Storage + Metadata in PostgreSQL |
| Domain Events | Kafka after transactional commit |
| Read Models | Derived Projections |
| Cache | Redis |
What I Would Build Today
For the first serious production version:
Application
FastAPI
API
REST + WebSockets
Database
PostgreSQL
Financial Model
Double-entry ledger
Real-Time
WebSockets
Ephemeral State
Redis
Event Backbone
Kafka
Reliable Event Publication
Transactional Outbox
Async Processing
Celery
Files
S3-compatible Object Storage
Read Architecture
CQRS projections
Payment Integration
Provider abstraction + webhook verification
Security
JWT / session strategy
Tenant isolation
RBAC / resource authorization
Observability
Structured Logs
Metrics
Distributed Tracing
What I Would Avoid Initially
Microservices Everywhere
Do not begin with:
Messaging Service
Contract Service
Wallet Service
Escrow Service
Dispute Service
Payment Service
Notification Service
as seven independent deployments unless the scale or organizational boundaries justify them.
Start with a modular monolith.
Extract boundaries later.
Mutable Wallet Balance as Truth
Avoid:
wallet.balance += amount
as the authoritative financial operation.
Use:
Ledger Transaction
+
Ledger Entries
and derive the balance.
Calling Payment Provider Inside Database Transaction
Avoid:
with transaction.atomic():
ledger.release()
provider.payout()
Use:
Ledger Transaction
↓
Outbox
↓
Payout Worker
↓
Provider
↓
Webhook / Reconciliation
Treating WebSocket Delivery as Persistence
Avoid:
await websocket.send_json(message)
without durable persistence.
Persist first.
Deliver second.
Using Redis Pub/Sub as Durable Messaging
Avoid relying on Pub/Sub as the canonical message history.
Redis Pub/Sub is appropriate for ephemeral fanout.
Durable business events should use:
Kafka
and durable messages should live in:
PostgreSQL
Treating Client-Side Payment Success as Financial Success
Avoid:
Browser says payment succeeded
↓
Mark escrow funded
Use:
Verified provider event
↓
Idempotent financial transaction
↓
Escrow funded
Allowing Chat to Mutate Financial State
Avoid:
Client message:
"Okay, release the money."
automatically becoming:
Release Funds
Financial actions must be explicit commands with authorization and state validation.
Architecture Decision Records
Important decisions should be captured as ADRs.
ADR-001
Financial state uses a double-entry ledger.
ADR-002
Wallet balances are projections, not source of truth.
ADR-003
Escrow uses explicit state transitions.
ADR-004
Contract terms are immutable versioned objects.
ADR-005
Acceptance is bound to a specific contract version.
ADR-006
Messages are persisted before WebSocket fanout.
ADR-007
Redis Pub/Sub is used only for ephemeral real-time fanout.
ADR-008
Kafka is used for durable domain-event distribution.
ADR-009
Transactional Outbox guarantees reliable event publication.
ADR-010
Payment provider calls never execute inside financial database transactions.
ADR-011
Payment webhooks are verified and idempotently processed.
ADR-012
Unknown external payment states require reconciliation before retry.
ADR-013
Opening a dispute synchronously freezes relevant escrow funds.
ADR-014
Read models are derived and rebuildable.
ADR-015
Initial implementation uses a modular monolith.
ADR-016
Financial operations require explicit concurrency control.
Evolution Path
Basic Messaging
│
▼
Persistent Conversations
│
▼
Versioned Contracts
│
▼
Milestone Workflows
│
▼
Double-Entry Wallet
│
▼
Escrow Allocation
│
▼
Payment Provider Integration
│
▼
Transactional Outbox
│
▼
Kafka Event Backbone
│
▼
Automated Dispute Freeze
│
▼
CQRS Read Models
│
▼
Multi-Instance WebSocket Gateway
│
▼
Payment Reconciliation
│
▼
Advanced Settlement
│
▼
Selective Domain Extraction
The important architectural decision is not:
"Use Kafka."
or:
"Use WebSockets."
It is:
Separate the consistency boundaries before scaling the infrastructure.
Real-World Architectural Parallels
Upwork
The platform demonstrates the value of connecting collaboration, contracts, milestones, and protected payments rather than treating them as unrelated workflows.
Escrow.com
The escrow model demonstrates the importance of conditional fund custody and explicit release conditions.
Intercom
Real-time communication systems demonstrate the engineering challenges around persistent connections, message delivery, presence, and large-scale fanout.
NexusFlow combines these architectural categories into one platform while keeping their underlying consistency requirements separate.
The Architectural Core
Everything eventually converges on this model:
User Intent
│
▼
Command
│
▼
Authentication
│
▼
Authorization
│
▼
Domain Validation
│
▼
Transactional Boundary
│
┌─────────────┴──────────────┐
│ │
▼ ▼
Business State Ledger State
│ │
└─────────────┬──────────────┘
│
▼
Outbox
│
▼
Kafka
│
┌────────────────┼────────────────┐
▼ ▼ ▼
Notifications Read Models Integrations
│ │ │
▼ ▼ ▼
WebSocket Dashboards Payment Provider
For messaging:
Command
↓
Persist Message
↓
Outbox
↓
Event
↓
Redis Fanout
↓
WebSocket
For escrow:
Command
↓
Validate State
↓
Lock Rows
↓
Ledger Transaction
↓
Escrow State Change
↓
Outbox
↓
Kafka
↓
Payout / Notification / Reporting
The two paths intentionally differ.
That difference is the architecture.
The Fundamental Trust Model
NexusFlow does not attempt to make users trustworthy.
It attempts to make the system trustworthy even when users are not.
Client
│
├── Wants delivery before payment
│
▼
Escrow
│
├── Locks funds
│
▼
Milestone
│
├── Defines obligation
│
▼
Delivery
│
├── Creates evidence
│
▼
Approval / Dispute
│
├── Controls release
│
▼
Ledger
│
└── Makes financial movement auditable
Trust therefore emerges from the architecture:
Contract
+
Evidence
+
State
+
Authorization
+
Ledger
+
Escrow
+
Audit
rather than from assumptions about either party.
What Makes NexusFlow an Architecture
NexusFlow is not:
FastAPI
+
PostgreSQL
+
Redis
+
Kafka
+
WebSockets
That is a technology stack.
The architecture is the relationship between those technologies and the business invariants:
Real-Time Communication
↓
WebSockets + Redis
Durable Messages
↓
PostgreSQL
Contract Integrity
↓
Versioned Contract State
Financial Correctness
↓
Double-Entry Ledger
Conditional Payment
↓
Escrow State Machine
Concurrent Release Protection
↓
Database Locking + Idempotency
External Payment Uncertainty
↓
Payment Intent + Webhooks + Reconciliation
Reliable Events
↓
Transactional Outbox + Kafka
Dispute Protection
↓
Synchronous Fund Freeze
High-Scale Queries
↓
CQRS Read Models
Distributed Failure
↓
Retries + DLQ + Reconciliation
Tenant Security
↓
Organization + Resource Authorization
That is the difference between a system that merely has components and a system that has an architecture.
Key Takeaways
NexusFlow treats communication, contracts, and money as different domains that happen to participate in the same business transaction.
A production-grade implementation provides:
- Real-time messaging through horizontally scalable WebSocket gateways.
- Durable communication history independent of WebSocket availability.
- Ephemeral presence and typing through Redis rather than the transactional database.
- Versioned contracts where every acceptance references an exact contract version.
- Milestone state machines that explicitly govern delivery, review, approval, revision, and dispute.
- Double-entry financial accounting rather than a mutable wallet balance.
- Escrow allocations that distinguish funded, locked, disputed, released, and refunded money.
- Concurrency-safe fund transitions using transactional locking and idempotency.
- Payment intents that separate internal financial state from external provider operations.
- Verified and idempotent webhooks for asynchronous payment confirmation.
- Asynchronous payout processing rather than external payment calls inside database transactions.
- Automatic dispute freezes that prevent financial races once a dispute begins.
- Evidence graphs connecting contracts, milestones, messages, files, approvals, and financial records.
- Transactional Outbox for reliable event publication.
- Kafka for durable domain-event distribution.
- Redis Pub/Sub for ephemeral real-time fanout rather than durable business events.
- CQRS projections for high-performance dashboards and reporting.
- Reconciliation processes for detecting divergence between internal and external systems.
- Multi-tenant authorization enforced at organization, resource, and operation boundaries.
- Audit and correlation IDs that make complex transactions traceable end to end.
- A modular monolith starting point that can evolve into selective service extraction when scale actually requires it.
The fundamental philosophy is:
Communication creates context. Contracts create obligations. Escrow creates controlled commitment. The ledger creates financial truth. Events connect the domains without collapsing their consistency boundaries.
That is what turns NexusFlow from a chat application with payments into a genuine transaction architecture for digital work relationships.


