ADR-0003: Loyalty Programme Integration
Field | Value |
|---|---|
Status | Proposed — Pending Commercial Partnership |
Date | 2026-04-27 |
Author | Alex Henshaw |
Deciders | BlueGuardian Co |
Migrated from | Project-wide ADR-009 |
Context
Subscription churn is one of the most significant risks for a SaaS platform with recurring revenue. Post-launch, BlueGuardian intends to reduce churn by integrating with established consumer loyalty programmes, so that users earn reward points on each successful subscription renewal. This creates a positive re-engagement loop that extends beyond the intrinsic value of the platform itself.
The integration targets the consumer (TrueWard) tier in the first instance, where individual subscribers have personal loyalty memberships. Commercial/B2B tiers (Business, Enterprise) are out of scope for the initial rollout.
Loyalty programme partnerships are not self-service: they require a negotiated commercial agreement with the programme operator before any technical integration is possible. This ADR therefore establishes the target architecture and provider abstraction to be implemented during the pre-launch build phase, so that when the business development process concludes, integration is a matter of implementing a known interface rather than a design-time decision.
This ADR does not commit to a specific provider, points rate, or integration timeline. Those decisions are dependent on the outcome of commercial negotiations and are tracked separately.
Decision
CEREBRAL STRATUM will implement a pluggable loyalty provider abstraction within the Billing & Entitlement Sync service. Points awards will be triggered by successful subscription renewal events originating from Shopify, and dispatched asynchronously using the outbox pattern to ensure renewal processing is never blocked by loyalty award failures.
The system will be region-gated: only users whose billing address region maps to a supported provider will have points awarded. Provider registration in the user profile is opt-in and user-initiated.
The integration will be disabled by default via a feature flag (LOYALTY_INTEGRATION_ENABLED) and fully absent from community entitlement mode deployments.
The primary target programme for the Australian market is Flybuys (Loyalty Pacific Pty Ltd), subject to a commercial partnership agreement.
Provider Abstraction
All loyalty programme integrations implement a common interface, housed in the Billing & Entitlement Sync service:
Provider implementations are CDI beans and discovered automatically. The LoyaltyProviderRegistry resolves the correct provider at runtime based on user.region and the presence of a linked membership.
Data Model
Two new tables are introduced in the cerebralstratum schema (Liquibase-managed):
loyalty_memberships
Stores the link between a platform user and their external loyalty programme membership.
Column | Type | Notes |
|---|---|---|
|
| PK |
|
| FK → |
|
| e.g. |
|
| Loyalty programme member number/token |
|
| When the user linked the account |
|
| User can deactivate without deleting |
Unique constraint: (user_id, provider_id) — one membership per provider per user.
loyalty_events
Append-only audit log of all points award attempts. Enables idempotency enforcement and dispute resolution.
Column | Type | Notes |
|---|---|---|
|
| PK |
|
| FK → |
|
| Discriminator |
|
| Source-of-truth event identifier |
|
| Points awarded (0 if failed) |
|
|
|
|
| |
|
| Provider's confirmation reference, nullable |
|
| Populated on |
Unique constraint: (user_id, shopify_order_id, provider_id) — enforces idempotency at the database level.
Renewal Flow
Points award is a side effect of renewal, not part of the critical path. The Shopify subscription_contract_renewed webhook is the trigger.
Key invariants:
Webhook handler always returns
200regardless of loyalty award outcome.A failure in
LoyaltyAwardServicemust never propagate toEntitlementSyncService.FAILEDevents are retryable via a Quarkus@Scheduledtask (configurable retry window, defaulting to 3 attempts with exponential backoff).SKIPPEDis written when the user has no linked membership or is not in a supported region — this is not an error condition.
Region Gating
The Region enum maps to ISO 3166-1 alpha-2 country codes derived from the Shopify billing address at subscription creation time:
Provider-to-region mapping at launch (subject to commercial outcomes):
Region | Programme | Status |
|---|---|---|
| Flybuys | Pending commercial agreement |
| Flybuys NZ / Airpoints | Under evaluation |
| TBD | No dominant equivalent identified |
| TBD | Fragmented; deferred |
Alternatives Considered
1. Direct Shopify integration via Shopify Flow + external app
Shopify Flow could trigger a webhook on renewal. However, this couples the loyalty logic to Shopify's workflow engine rather than the platform's own service layer, making it harder to enforce idempotency, add retry logic, or support multiple regions. Rejected in favour of server-side ownership.
2. Client-side loyalty number entry only, no server-side API call
Some programmes accept points claims via batch file upload rather than real-time API. This would simplify the integration but introduce significant delay in point crediting and create a reconciliation burden. Deferred as a fallback if the target provider does not offer a real-time earn API.
3. Build a proprietary BlueGuardian points system
Could offer points redeemable against subscription discounts or hardware purchases. Provides full control but requires significant product effort and has no established member base. Rejected for initial phase; may be revisited as a complement to external programmes.
4. Single hardcoded Flybuys integration
Simpler to implement but creates a rework obligation when additional regions are onboarded. The provider abstraction has minimal overhead and eliminates future design-time risk. Rejected.
Consequences
Positive:
Loyalty integration does not block or complicate the subscription renewal critical path.
Pluggable design means additional regional providers can be added without modifying existing code.
loyalty_eventstable provides a complete audit trail for member dispute resolution and internal reconciliation.Idempotency at the database level (
UNIQUEconstraint on(user_id, shopify_order_id, provider_id)) protects against Shopify webhook redelivery.Feature flag means the integration can be shipped in code before any commercial partnership exists, and toggled on per-environment when ready.
Negative / Trade-offs:
A commercial agreement with Loyalty Pacific (Flybuys) is a prerequisite for AU market delivery. Architecture work can proceed but the feature cannot go live until BD concludes.
The actual Flybuys earn API shape, authentication mechanism, and points rate are all subject to negotiation — the
LoyaltyProviderinterface may require adjustment once the API is known.Adding a
loyalty_membershipsentry to the user profile flow adds UX surface area. Needs a dedicated account-linking screen in the KMP client.Users in unsupported regions (US, EU at launch) will not benefit from this feature, which may create perceived inequity.
Open Items
[ ] Initiate commercial partnership discussion with Loyalty Pacific (Flybuys) — BD action, not engineering.
[ ] Evaluate Airpoints (Air New Zealand) as an alternative/complement for the NZ market.
[ ] Determine points earn rate per renewal (likely expressed as points per AUD spent, subject to negotiation).
[ ] Confirm Flybuys earn API authentication model (OAuth2, API key, or client certificate) —
LoyaltyProviderinterface may need adjustment.[ ] Design the account-linking UX in KMP — the
loyalty_membershipsentry should be user-initiated from an account settings screen, with explicit privacy notice.[ ] Assess privacy implications of storing
external_member_tokenunder AU Privacy Act and GDPR. Consider whether it should be stored encrypted at rest (likely yes, via PostgreSQLpgcryptoor application-layer encryption).[ ] Define the retry schedule and alerting threshold for
FAILEDloyalty events.[ ] Decide whether to surface loyalty event history to users in the KMP client.
Forward Pointers
Account-linking UX will need to be considered alongside the KMP client account settings design.
If a proprietary BlueGuardian points system is revisited in future, it should extend rather than replace this abstraction — the
LoyaltyProviderinterface can accommodate a first-party provider implementation.