The Web App of my CSL Datahub implementation
How a legacy PHP system remains the source of truth in a Datahub

Series: Designing a Microservice-Friendly Datahub
PART III — CASE STUDY: MY CSL DATAHUB IMPLEMENTATION
Previous: High-Level Architecture Overview of my CSL Datahub implementation
Next: Redis Streams as an Event Buffer in Datahub
Every Datahub needs an anchor.
In the CSL system, that anchor is not a stream, a broker, or a processor—it’s a legacy PHP web application.
This article focuses on the most important and most easily misunderstood part of the architecture: why the CSL Web App remains the source of truth, how it emits events safely, and why protecting it from infrastructure complexity is a deliberate design choice, not a limitation.
Disclaimer (Context & NDA)
The CSL Web App and Datahub integration described here were designed and implemented in 2021. While the architectural principles remain valid, some implementation details could be modernized today. To respect NDA requirements, domain-specific logic, schemas, and proprietary workflows are intentionally generalized.
The Role of the CSL Web App
The CSL Web App is built on PHP using the Yii framework (Humhub). It predates the Datahub, predates microservices, and predates event-driven architecture.
Yet it remains:
Business-critical
Actively used
Rich in domain logic
The only place where authoritative state is written
This makes it exactly the kind of system that modern architectures must learn to work with—not replace.

Its responsibilities are intentionally narrow:
Accept user input
Apply domain rules
Persist state to MySQL
Announce state changes
Everything else is someone else’s job.
MySQL: The Single Source of Truth
In the CSL architecture, MySQL is authoritative.
This means:
All writes happen here
All business invariants are enforced here
All other systems derive their view from here
The Web App does not attempt to:
Maintain denormalized read models for other services
Synchronize databases
Push data directly into other systems
A typical transactional flow looks like this:
$db->beginTransaction();
try {
$user->email = $newEmail;
$user->updated_at = time();
$user->save();
$db->commit();
} catch (Exception $e) {
$db->rollBack();
throw $e;
}
Only after this transaction commits does the system talk about events.
Events Describe Facts, Not Intent
One of the most important architectural decisions was that the Web App never emits commands—only events.
An event is emitted after state changes, never before.
$redis->xAdd(
'csl:events',
'*',
[
'type' => 'user.updated',
'user_id' => $user->id,
'occurred_at' => time()
]
);
This event says one thing only:
“A user was updated.”
It does not say:
What other systems should do
Which systems should react
Whether the update succeeded elsewhere
This keeps the Web App free from downstream assumptions.
Why Redis Streams, Not RabbitMQ
A natural question arises:
Why doesn’t the CSL Web App publish directly to RabbitMQ?
The answer is architectural protection.
Direct broker integration would mean:
Network dependencies inside request flows
Failure modes the app cannot control
Operational concerns leaking into core logic
Coupling the app to infrastructure decisions
Instead, Redis Streams act as a local, low-risk boundary.

Redis offers:
Fast, in-process publishing
Minimal configuration
Simple failure behavior
Familiar operational semantics
If Redis is temporarily unavailable:
The app can retry locally
Failures are contained
Users are not blocked by downstream outages
The Web App talks to Redis because Redis behaves like local infrastructure, not a distributed dependency.
Redis Streams as an Emission Layer
Inside the CSL Web App, Redis Streams are treated as an append-only log.
Key characteristics:
No consumer awareness
No routing logic
No retries handled here
No business interpretation

The Web App’s only concern is:
“Record that something happened.”
This keeps the emission logic trivial—and therefore safe.
REST APIs: Explicit, Controlled Boundaries
The CSL Web App does expose REST APIs—but only for specific purposes.
These APIs are used by:
The Processor service
Administrative tools
Controlled integrations
They are not used for event propagation.
Example API handler:
public function actionUpdateStatus($id)
{
$model = User::findOne($id);
$model->status = 'processed';
$model->save();
return ['success' => true];
}
REST APIs are:
Synchronous
Intent-driven
Explicitly permissioned
They belong to the control plane, not the data plane.
Why the Web App Does Not Orchestrate
The CSL Web App never:
Calls other services in response to events
Waits for downstream acknowledgments
Coordinates multi-system workflows
That work is intentionally delegated.
Why?
Because orchestration:
Introduces tight coupling
Makes failures contagious
Forces the app to care about others’ availability

The Web App’s job ends when it:
Validates input
Writes state
Emits an event
Anything beyond that risks turning it into a distributed coordinator—something it was never designed to be.
Protecting the Core System
The most important outcome of this design is risk isolation.
If:
RabbitMQ is down
The Processor crashes
External modules misbehave
The CSL Web App:
Continues to function
Continues to accept user input
Continues to write correct state
This is not accidental. It is the primary success criterion.

Modern patterns are valuable only if they do not destabilize the core business system.
Legacy Systems Can Be First-Class Citizens
The CSL Web App is not “modernized” by:
Rewriting it
Forcing new paradigms into it
Turning it into a microservice
It is modernized by:
Giving it clear boundaries
Letting it speak in facts
Shielding it from infrastructure complexity

This is how legacy systems survive architectural evolution without becoming liabilities.
Mental Model Recap
The CSL Web App:
Owns truth
Emits facts
Accepts intent via APIs
Avoids downstream dependencies
Everything else reacts.

That asymmetry is intentional—and powerful.
Where We Go Next
Now that we’ve established the Web App as the source of truth, the next question is what happens after an event is emitted.
In the next article, Redis Streams as an Event Buffer, we’ll zoom in on the first hop beyond the core system—why buffering matters, how consumer groups work, and how Redis absorbs pressure so the rest of the Datahub can breathe.
Legacy systems don’t block modern architecture.
Unclear boundaries do.






