webapp Case study
Bale / Rubika / Eitaa Customer Messaging
Reaching customers in Iran means Bale, Rubika and Eitaa rather than the platforms most messaging tooling assumes. Each has its own API and its own failure behaviour, and none of them should be visible to the code that decides what to send.
The business problem
Messaging integrations rot when provider details leak into business logic. Add a second platform and you either duplicate the campaign code or thread conditionals through it. Customer messaging also carries obligations that are not technical. Opt-out has to be honoured permanently, and OTP traffic must not share a path with campaign traffic. Someone also has to approve a template before thousands of people receive it.
What I delivered
- a2-bale-customer-hub, a WordPress plugin connecting customer records to Bale bot and Safir messaging workflows.
- Provider-specific logic isolated behind platform and OTP provider classes, so adding or replacing a channel never reaches into campaign code.
- Campaign queues with templates, so a send is a reviewable object rather than a loop that has already started.
- Opt-out stored as a record rather than inferred, because an unsubscribe that depends on a query being written correctly is one bad query away from a compliance problem.
- Contact import that pulls from WooCommerce and from CRM contact sources wherever they exist.
- Support request handling on the same customer records, so an inbound question and an outbound campaign share a view of the customer.
Technical approach
- The provider boundary is the architecture. Everything above it talks about customers, templates and queues; everything below it knows about one platform's API.
- I keep OTP behind its own provider abstraction, separate from campaign messaging. The two differ in urgency, in failure handling, and in what it costs when someone confuses them.
- Queues rather than direct sends, so I can inspect a campaign, pause it and reason about it instead of watching a request that either finished or did not.
- I check opt-out as a positive record at send time. Anything less makes correctness depend on every future query being written carefully.
- I keep tokens, webhook secrets and contact exports out of the repository, which is also why the public description of this work stays at architecture level.
Result and evidence
Customer messaging runs across the Iranian platforms the business actually needs. The provider details stay contained, and the structure enforces the customer-protection rules rather than leaving them to convention.
Commercial value
Messaging is where a commerce business damages its relationship with customers fastest. Building the review step and the opt-out record in from the start is much cheaper than retrofitting them after an incident.
Readable implementation brief
implementation_brief {
project: "Bale / Rubika / Eitaa Customer Messaging"
plugin: "a2-bale-customer-hub (WordPress, PHP 7.4+)"
boundary: "platform + OTP provider classes; campaign code
never sees a provider API"
otp: "separate provider abstraction from campaign sends"
queues: "campaigns are inspectable objects, not loops"
optout: "stored record, checked at send; never inferred"
sources: "WooCommerce + A2/Yaghout CRM contact import"
excluded_from_repo: "tokens, webhook secrets, OTP secrets,
contact lists, exports, logs"
}What this project shows
The adapter boundary is ordinary good practice; what I would point at is putting OTP behind a separate provider from campaigns. Mixing them is a common shortcut and it fails in the worst possible way.
Treating opt-out as stored state rather than a derived condition is the kind of decision that looks like overkill until the day it does not.