Version: 5.0 | Date: December 23, 2025
This document covers the main app-server communication systems:
┌─────────────────┐ ┌─────────────────┐
│ APP STARTS │ │ SERVER │
└────────┬────────┘ └────────┬────────┘
│ │
┌────▼──────────────┐ │
│ Test Internet │ │
│ Speed (3 tests) │ │
│ Minimum: 1.5 Mbps │ │
└────┬──────────────┘ │
│ │
│ (1) Send Handshake with JWT │
├──────────────────────────────────────────────>│
│ Authorization: Bearer {JWT} │
│ {analytics, device_id, purchases} │
│ │
│ (2) Return Manifest + User Data │
│<───────────────────────────────────────────────┤
│ {"json_manifest": {...}, │
│ "ai_usage": {...}, │
│ "subscription": {...}} │
│ │
┌────▼──────────────┐ │
│ Update user_profile│ │
│ with ai_usage & │ │
│ subscription │ │
└────┬──────────────┘ │
│ │
┌────▼──────────────┐ │
│ Analyze manifest: │ │
│ - Courses to ADD │ │
│ - Courses to UPDATE│ │
│ - Courses to DELETE│ │
│ - System JSONs │ │
└────┬──────────────┘ │
│ │
┌────▼──────────────┐ │
│ Check storage & │ │
│ network type │ │
└────┬──────────────┘ │
│ │
┌────▼──────────────┐ ┌──────────────┐ │
│ Download new │─────────>│ Cloudflare │ │
│ courses (with │<─────────│ R2 │ │
│ user confirmation)│ └──────────────┘ │
└────┬──────────────┘ │
│ │
┌────▼──────────────┐ │
│ Batch UPDATE │ │
│ courses & system │ │
│ JSONs │ │
└────┬──────────────┘ │
│ │
┌────▼──────────────┐ │
│ DELETE removed │ │
│ courses (safe) │ │
└────┬──────────────┘ │
│ │
[Sync Complete] │
Before initiating sync, the app performs 3 speed tests using Cloudflare CDN (1MB download each). The average speed must be ≥1.5 Mbps to proceed. If the speed test server is unreachable, sync proceeds anyway.
Speed Test URL: https://speed.cloudflare.com/__down?bytes=1000000 Minimum Speed: 1.5 Mbps (averaged across 3 tests)
The app builds the handshake payload with analytics data, device ID, and pending purchases. Note: user_id is NOT included - it's extracted from JWT on the server.
{
"analytics": {
"current_course": "C14ENB",
"voice": "Female",
"background": "None",
"induction": "Randomize",
"deepening": "Randomize",
"audio_usage": {
"1": {"title": "How to Use These Sessions", "no_of_plays": 1},
"2": {"title": "Release Social Anxiety", "no_of_plays": 3}
}
},
"device_id": "unique-device-identifier-string",
"purchases": [
{
"product_id": "h01enb",
"purchase_id": "GPA.1234-5678",
"platform": "android",
"status": "pending_validation"
}
]
}
The app sends the payload to the handshake endpoint with JWT authentication:
URL: https://kagtryxgjwavupzlmlzv.supabase.co/functions/v1/handshake
Method: POST
Headers:
Authorization: Bearer {user_jwt_from_supabase_session}
Content-Type: application/json
Dev Mode URLs:
Emulator: http://localhost:8080/handshake
Device: http://{configured_ip}:8080/handshake
Server returns JSON manifest, AI usage data, and subscription status. All JSON content uses LinkedHashMap to preserve field order for consistent checksums.
{
"json_manifest": {
"background.json": {
"checksum": "a1b2c3d4...",
"content": { /* full JSON content */ }
},
"core_segments.json": {
"checksum": "e5f6g7h8...",
"content": { /* full JSON content */ }
},
"FAQs.json": { ... },
"shop.json": { ... },
"ads.json": { ... },
"C14ENB.json": { ... }
},
"ai_usage": {
"weekly_limit_credits": 10,
"weekly_used_credits": 2,
"weekly_reset_date": "2025-12-30T00:00:00Z",
"bank_limit_credits": 100,
"bank_used_credits": 5
},
"subscription": {
"subscription_id": "sub_12345",
"external_subscription_id": "GPA.sub-12345",
"subscription_type": "premium",
"status": "active",
"start_date": "2025-01-01T00:00:00Z",
"expiry_date": "2026-01-01T00:00:00Z",
"auto_renew": true,
"plan": "monthly-plan"
}
}
After receiving the response, the app updates user_profile.json with:
ai_usage object with weekly/bank credit limits and usagesubscription object with subscription detailsuser_group set to "premium" if subscription is active, otherwise "free"The orchestrator analyzes the manifest to categorize operations:
| Category | Condition | Action |
|---|---|---|
| Courses to ADD | In server manifest, not in local (and not disabled) | Full download with user confirmation |
| Courses to UPDATE | Checksum differs | Batch update changed files |
| Courses to DELETE | In local, not in server manifest | Safe deletion (profile first) |
| System JSONs | Checksum differs | Batch update |
For each new course to ADD, the app:
For courses and system JSONs needing updates:
For courses no longer in entitlements:
| Phase | Description |
|---|---|
analyzing | Analyzing sync requirements |
checkingStorage | Verifying device storage space |
waitingForUserConfirmation | Waiting for user to confirm download |
downloadingCourse | Downloading course files |
addingCourses | Adding new courses to profile |
updatingSystem | Updating system JSONs |
updatingCourses | Updating existing courses |
deletingCourses | Removing outdated courses |
updatingProfile | Updating user profile |
completed | Sync completed successfully |
failed | Sync failed |
cancelledByUser | User cancelled sync |
These system JSONs cannot be deleted and are always synced:
ads.jsonbackground.jsoncore_segments.jsonFAQs.jsonshop.jsonRisk: Network failure during course download
Mitigation:
Risk: Device may not have space for new files
Mitigation:
Risk: Cloudflare speed test server unreachable
Mitigation:
| Platform | Format | Example |
|---|---|---|
| Android (Google Play) | Lowercase course code | h01enb |
| iOS (App Store) | Bundle prefix + lowercase | me.hypnoelp.app.h01enb |
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ APP │ │ STORE API │ │ SERVER │
│ │ │ (Google/Apple) │ │ │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
│ (1) User selects course │ │
│ App normalizes product ID│ │
│ │ │
│ (2) buyConsumable() │ │
├─────────────────────────────>│ │
│ │ │
│ (3) PurchaseDetails │ │
│ (NOT acknowledged yet) │ │
│<─────────────────────────────┤ │
│ │ │
│ (4) Check if already recorded│ │
│ in user_profile.json │ │
│ │ │
│ (5) Send to /validate_purchase │
├──────────────────────────────────────────────────────────>│
│ {purchase_token, platform, │ │
│ product_id, course_code, │ │
│ voice} │ │
│ │ (6) Validate │
│ │<─────────────── with Store │
│ │ "Valid" ──────────────────>│
│ │ │
│ │ (7) Update DB │
│ │ │
│ (8) Success Response │ │
│<──────────────────────────────────────────────────────────┤
│ │ │
│ (9) NOW acknowledge purchase │ │
├─────────────────────────────>│ │
│ completePurchase() │ │
│ │ │
│ (10) Add to user profile │ │
│ Trigger sync │ │
Before initiating purchase, the app:
restorePurchases() to check for existing purchases// Query product details from store
final response = await _iap.queryProductDetails({normalizedProductId});
// Initiate consumable purchase (repeatable)
final result = await _iap.buyConsumable(purchaseParam: purchaseParam);
The app waits up to 20 minutes for purchase completion via purchase stream. Duplicate purchases are detected by checking user_profile.json.
| Field | Android | iOS |
|---|---|---|
| Verification Data | purchase_token from serverVerificationData |
receipt_data from method channel |
| Receipt Source | Google Play Billing | Bundle.main.appStoreReceiptURL |
| Method Channel | N/A | app_receipt → getAppReceipt |
{
"product_id": "h01enb",
"purchase_id": "GPA.1234-5678-9012",
"transaction_date": "1703347200000",
"status": "PurchaseStatus.purchased",
"platform": "android",
"purchase_token": "token-from-google-play",
"course_code": "H01ENB",
"voice": "Female"
}
For iOS:
{
"product_id": "me.hypnoelp.app.h01enb",
"purchase_id": "1000000123456789",
"transaction_date": "1703347200000",
"status": "PurchaseStatus.purchased",
"platform": "ios",
"receipt_data": "base64-encoded-app-receipt...",
"course_code": "H01ENB",
"voice": "Female"
}
Server validates with Google Play API or Apple's verifyReceipt API, then returns:
{
"status": "success",
"message": "Course purchase validated",
"acknowledged": true,
"course": {
"course_code": "H01ENB",
"title": "Confidence Building",
"voice": "Female",
"purchased_at": "2025-12-23T12:00:00Z"
}
}
// Called ONLY after server confirms purchase is valid await _iap.completePurchase(purchaseDetails);
user_profile.jsonhypnoelp_subscriptions) with multiple base plansme.hypnoelp.app.subscription_1month)| Setting | Value |
|---|---|
| Product ID | hypnoelp_subscriptions |
| Base Plans | Multiple (monthly-plan, quarterly-plan, yearly-plan, etc.) |
| Offer Tags | Configured per base plan |
| Setting | Value |
|---|---|
| Product ID | me.hypnoelp.app.subscription_1month |
| Duration | 1 month |
| Billing Period | P1M |
┌─────────────────┐ ┌─────────────────┐
│ APP │ │ SERVER │
└────────┬────────┘ └────────┬────────┘
│ │
│ (1) querySubscriptionPlans() │
│ Returns: plans with pricing, free trials │
│ │
│ (2) User selects plan │
│ │
│ (3) purchaseSubscription() │
│ - Android: GooglePlayPurchaseParam │
│ with ChangeSubscriptionParam (upgrade) │
│ - iOS: PurchaseParam │
│ │
│ (4) buyNonConsumable() │
│ Wait for completion (20 min timeout) │
│ │
│ (5) Extract purchase data │
│ - Convert date to ISO 8601 │
│ - Get receipt/token │
│ │
│ (6) Send to /validate_purchase │
├──────────────────────────────────────────────>│
│ {platform, purchase_token/receipt_data, │
│ base_plan_id, transaction_date (ISO 8601)} │
│ │
│ (7) Validation Response │
│<──────────────────────────────────────────────┤
│ │
│ (8) Acknowledge purchase │
│ completePurchase() │
│ │
│ (9) Update user_profile.json │
│ - subscription object │
│ - user_group = "premium" │
Free trials are detected from pricing phases on Android:
// First pricing phase with priceAmountMicros == 0 indicates free trial
// Billing period format (ISO 8601 duration):
// P1D = 1 day, P7D = 7 days
// P1M = 1 month, P3M = 3 months
// P1Y = 1 year
if (firstPhase.priceAmountMicros == 0) {
// Free trial phase
freeTrialText = _formatBillingPeriod(billingPeriod);
// e.g., "First 7 days free"
}
// If user has active subscription, enable upgrade/downgrade
if (_activeSubscription != null) {
changeParam = ChangeSubscriptionParam(
oldPurchaseDetails: _activeSubscription!,
replacementMode: ReplacementMode.withTimeProration,
);
purchaseParam = GooglePlayPurchaseParam(
productDetails: googlePlayDetails,
changeSubscriptionParam: changeParam,
);
}
{
"product_id": "hypnoelp_subscriptions",
"purchase_id": "GPA.sub-1234-5678",
"transaction_date": "2025-12-23T12:00:00.000Z", // ISO 8601 format
"status": "PurchaseStatus.purchased",
"plan_title": "Monthly Plan",
"plan_description": "Premium access for 1 month",
"base_plan_id": "monthly-plan",
"offer_id": "",
"platform": "android",
"purchase_token": "token-from-google-play"
}
purchaseAICredits(productId) method{
"product_id": "ai_credits_100",
"purchase_id": "GPA.1234-5678-9012",
"transaction_date": "1703347200000",
"status": "PurchaseStatus.purchased",
"platform": "android",
"purchase_token": "token-from-google-play"
}
After successful AI credits purchase, the server updates the user's AI usage:
{
"ai_usage": {
"weekly_limit_credits": 10,
"weekly_used_credits": 0,
"weekly_reset_date": "2025-12-30T00:00:00Z",
"bank_limit_credits": 200, // Increased by purchase
"bank_used_credits": 0
}
}
The app sends logging payloads with the following fields:
{
"user_id": "user_9372328",
"log_type": "sync_module",
"message": "Sync completed successfully",
"origin": "app",
"created_at": "2025-12-23T14:30:00Z"
}
{
"user_id": "user_7339483",
"log_type": "sync_module",
"message": "Speed test: 5.23 Mbps (3 attempts average)",
"origin": "app",
"created_at": "2025-12-23T14:31:00Z"
}
{
"user_id": "user_7339483",
"log_type": "sync_module",
"message": "Sync skipped: slow connection (0.87 Mbps)",
"origin": "app",
"created_at": "2025-12-23T14:35:00Z"
}
{
"user_id": "user_7339483",
"log_type": "sync_module",
"message": "No connection. Interrupted downloading file: H01ENB_audio_01.mp3",
"origin": "app",
"created_at": "2025-12-23T14:35:00Z"
}
{
"user_id": "user_u38439493",
"log_type": "billing",
"message": "Course purchase initiated: H01ENB (Android)",
"origin": "app",
"created_at": "2025-12-23T14:40:00Z"
}
{
"user_id": "user_u38439493",
"log_type": "billing",
"message": "Course purchase validated: H01ENB - acknowledged to Google Play",
"origin": "app",
"created_at": "2025-12-23T14:41:00Z"
}
{
"user_id": "user_u38439493",
"log_type": "billing",
"message": "Purchase validation failed: H01ENB - NOT acknowledged (will auto-refund)",
"origin": "app",
"created_at": "2025-12-23T14:41:00Z"
}
{
"user_id": "user_u38439493",
"log_type": "user_report",
"message": "Course H23ENB fails to load after purchase validation",
"origin": "app",
"created_at": "2025-12-23T14:40:00Z"
}
sync_module - File synchronization eventsbilling - Payment, purchase, and subscription eventsauthentication - Login/logout eventsaudio_playback - Audio player eventsai_chat - AI chatbot usage eventserror - General application errorsuser_action - User interactions and navigationuser_report - User-reported issues and problems| Endpoint | Purpose | Auth |
|---|---|---|
/functions/v1/handshake |
Sync, user data, manifest | JWT Bearer |
/functions/v1/validate_purchase |
Course, subscription, AI credits validation | JWT Bearer |
/functions/v1/app_logs |
Event logging | JWT Bearer |
| Feature | Android | iOS |
|---|---|---|
| Course Purchase | ✅ Google Play Billing | ✅ StoreKit |
| Subscription | ✅ Multi-base plan | ✅ Monthly only |
| AI Credits | ✅ Consumable | ✅ Consumable |
| Upgrade/Downgrade | ✅ With proration | ❌ N/A (single plan) |
| Free Trial | ✅ From pricing phases | ✅ App Store managed |
| Receipt Retrieval | serverVerificationData | Method channel (app_receipt) |