Klaviyo Integration - Implementation Details
Architecture-level overview of the Klaviyo integration. Phase 5 (Mobile SDK) is complete. Phases 1-4 (Backend) remain.
Architecture Overview
Component Breakdown
1. Backend: Klaviyo Module --- NOT STARTED
Location: eli-backend-api/src/modules/klaviyo/
Structure:
klaviyo/
├── klaviyo.module.ts # Module definition
├── klaviyo.service.ts # API calls to Klaviyo
└── klaviyo.types.ts # TypeScript interfaces
Dependencies:
ShopifyModule- For email resolutionUsersModule- For user data
Responsibilities:
- Call Klaviyo API to update profiles
- Resolve correct email (Shopify vs app)
- Calculate test metrics
- Handle API errors gracefully
2. Backend: Hook Point --- NOT STARTED
Location: eli-backend-api/src/modules/readings/readings.controller.ts
Method: handleAnalysis()
When: After reading status changes to Completed
Pattern: Same as existing sendNotification() - fire and forget, don't block test completion
3. Backend: Email Resolution --- NOT STARTED
Uses: Existing shopify.service.ts
Method: getCustomerInfo(userId)
Returns: Customer email from Shopify
Flow:
- Check if user has
shopifyCustomerId - If yes, call Shopify GraphQL API
- Return
customer.emailAddress.emailAddress - If no, fall back to
user.emailfrom database
4. Backend: Test Stock Calculation --- NOT STARTED
Data Sources:
| Source | For | API/Table |
|---|---|---|
| Loop | Subscribers | BigQuery: eli_health_loopwork.subscriptions |
| Shopify | One-time buyers | shopify.service.getOrders() |
Logic:
- Subscribers:
remaining = testsInPlan - (totalCompleted % testsInPlan) - One-time:
remaining = quantityPurchased - totalCompleted
5. Batch Job: Daily Metrics --- NOT STARTED
Location: eli-kpi (new batch job)
Trigger: Cloud Scheduler, daily at 6 AM
Calculates:
days_since_last_test= TODAY - last_test_dateavg_tests_per_week= tests in last 30 days / 4.3
Flow:
6. Mobile: Klaviyo SDK --- DONE (EXM-996, Zeeshan, 2026-01-27)
Package: klaviyo-react-native-sdk
Initialization: IIFE in eli-app/src/services/klaviyo.ts (runs at module import time)
Environment: REACT_APP_KLAVIYO_API_KEY via .env-cmdrc.js → environment.klaviyoApiKey
Service (src/services/klaviyo.ts):
| Method | When Called | What It Does |
|---|---|---|
initialize() | Module load (IIFE) | Klaviyo.initialize(apiKey) |
setProfile(user) | Login (email, Google, Apple) + app re-auth | Sets email, externalId, firstName, lastName, timezone |
resetProfile() | Sign out | Clears Klaviyo identity |
setPushToken(token) | FCM token retrieval | Forwards Firebase push token to Klaviyo |
createEvent(event) | Measurement completion, notification opened | Sends custom events to Klaviyo |
Integration points across eli-app:
| File | Hook | Klaviyo Call |
|---|---|---|
services/auth.ts → checkIfAuthenticated() | App startup with existing session | setProfile(user) |
services/auth.ts → signIn() | Email/password login | setProfile(user) |
services/auth.ts → signInWithGoogle() | Google OAuth login | setProfile(user) |
services/auth.ts → signInWithApple() | Apple OAuth login | setProfile(user) |
services/auth.ts → signOut() | User logs out | resetProfile() |
services/messaging.ts → getFCMToken() | FCM token obtained | setPushToken(fcmToken) |
services/app.ts → onNotificationOpened | User opens a protocol reminder notification | createEvent('reminder_notification_opened') |
screens/.../MeasurementCalculating.tsx | Reading status = completed | createEvent('measurement_completed') |
Notification data enhancement (enables Klaviyo event tracking):
Local notifications (Notifee) now include userId, hormoneType, and type: 'protocol_reminder' in their data payload. Files changed: notification.ts, protocolNotifications.ts, useHormoneMeasurementState.tsx, ProtocolBuilderScreen.tsx, TestingProtocolsScreen.tsx, RemindersScreen.tsx.
Not yet implemented (Phase 5b):
- In-app message rendering (banners, modals from Klaviyo campaigns)
- Klaviyo push notification handling (
handlePushrouting) app_first_openedevent tracking
API Specifications
Klaviyo Profile Import API
Endpoint: POST https://a.klaviyo.com/api/profile-import/
Headers:
Authorization: Klaviyo-API-Key {KLAVIYO_API_KEY}
revision: 2024-10-15
Content-Type: application/json
Body Structure:
{
"data": {
"type": "profile",
"attributes": {
"email": "user@example.com",
"properties": {
"total_tests_completed": 5,
"last_test_date": "2025-01-26T10:30:00Z",
"first_test_completed": true,
"test_stock_percentage": 60,
"tests_remaining": 3,
"subscription_plan": "8_tests"
}
}
}
}
Rate Limits: 75 requests/second
Shopify Customer API
Already implemented in shopify.service.ts
Endpoint: https://account.eli.health/customer/api/2025-10/graphql
Returns: Customer ID, email, first/last name
Environment Variables
Backend (eli-backend-api/.env) --- NOT YET CONFIGURED:
KLAVIYO_API_KEY=pk_xxx # Private API key
KLAVIYO_API_REVISION=2024-10-15 # API version
Mobile (eli-app) --- DONE:
REACT_APP_KLAVIYO_API_KEY=xxx # Public API key for SDK init
Configured in .env-cmdrc.js (reads from process.env.REACT_APP_KLAVIYO_API_KEY), typed in src/types/env.d.ts, exposed via environment.klaviyoApiKey in src/config/environment.ts.
Note: The current config uses a single key from
process.envfor all environments. The Klaviyo Multi-Environment Guide specifies separate keys for dev/staging (test accounteli-health-test) and production (eli-health). Per-environment keys should be injected via the CI/CD pipeline.
Multi-environment setup: See Klaviyo Multi-Environment Guide for dev/staging/production configuration.
Error Handling Strategy
Key Principle: Klaviyo failures must NEVER block test completion.
Data Flow Summary
Real-Time (Every Test)
Properties Updated:
total_tests_completedlast_test_datefirst_test_completed(if first)days_to_first_test(if first)test_stock_percentagetests_remainingsubscription_plan
Daily Batch (6 AM)
Properties Updated:
days_since_last_testavg_tests_per_week
On Profile Update
Properties Updated:
user_genderuser_agepurchase_motivationhormone_tracking_goal
Integration Tips
Tip 1: Follow the Notification Pattern
Look at sendNotification() in readings.controller.ts:
- Fire and forget (don't await)
- Try-catch to prevent failures from bubbling
- Log success/failure
Tip 2: Use Existing Shopify Service
Don't reinvent email resolution:
shopifyService.getCustomerInfo(userId)
Returns email, first name, last name.
Tip 3: Module Structure
Copy shopify.module structure:
- Clean, minimal module definition
- Service handles all business logic
- Types in separate file
Tip 4: Idempotent Updates
Klaviyo profile-import is idempotent:
- Same email = update existing profile
- No need to check if profile exists first
Tip 5: Batch Job Location
Add to eli-kpi since:
- Already has BigQuery access
- Already has Cloud Scheduler setup
- Pattern exists for other batch jobs
Testing Strategy
Manual Testing
- Complete a test in staging app
- Check Klaviyo staging profile updated
- Verify all properties correct
- Test with Shopify-linked and unlinked users
Validation Queries
-- Check users with test data
SELECT email, total_tests, last_test_date
FROM `eli-health-prod.eli_health_biometricspublic.user` u
JOIN `eli-health-prod.eli_health_biometricspublic.reading` r
ON u.id = r.userId
WHERE r.status = 'completed'
LIMIT 10;
Monitoring
- Log all Klaviyo API calls
- Alert on auth errors (401)
- Track success rate (target: 99%+)
Related Documents
- Klaviyo Integration Guide - Complete business requirements
- Shopify Implementation - OAuth details
Document History
| Date | Author | Changes |
|---|---|---|
| 2026-01-27 | Chip | Updated with Phase 5 implementation (EXM-996, Zeeshan). Marked all component statuses. Updated architecture diagram and env vars. |
| 2025-01-26 | Chip | Initial implementation details |