# Push Notifications Architecture This document describes how push notifications work in the honeyDue API. ## Overview The honeyDue API sends push notifications directly to Apple Push Notification service (APNs) and Firebase Cloud Messaging (FCM) without any intermediate push server. This approach: - Reduces infrastructure complexity (no Gorush or other push server needed) - Provides direct control over notification payloads - Supports actionable notifications with custom button types ## Architecture ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ honeyDue API │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌─────────────────────┐ ┌───────────────────┐ │ │ │ Handlers │───▶│ NotificationService │───▶│ PushClient │ │ │ │ (HTTP API) │ │ │ │ │ │ │ └──────────────┘ │ - Create record │ │ ┌─────────────┐ │ │ │ │ - Get button types │ │ │ APNsClient │──┼───┼──▶ APNs │ ┌──────────────┐ │ - Send push │ │ │ (apns2) │ │ │ │ │ Worker │───▶│ │ │ └─────────────┘ │ │ │ │ (Asynq) │ └─────────────────────┘ │ │ │ │ └──────────────┘ │ ┌─────────────┐ │ │ │ │ │ FCMClient │──┼───┼──▶ FCM │ │ │ (HTTP) │ │ │ │ │ └─────────────┘ │ │ │ └───────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ## Components ### 1. Push Client (`internal/push/client.go`) The unified push client that wraps both APNs and FCM clients: ```go type Client struct { apns *APNsClient // iOS fcm *FCMClient // Android } ``` **Key Methods:** - `SendToIOS()` - Send to iOS devices - `SendToAndroid()` - Send to Android devices - `SendToAll()` - Send to both platforms - `SendActionableNotification()` - Send with iOS category for action buttons ### 2. APNs Client (`internal/push/apns.go`) Uses the [sideshow/apns2](https://github.com/sideshow/apns2) library for direct APNs communication: - **Token-based authentication** using .p8 key file - **Production/Sandbox** modes based on configuration - **Batch sending** with per-token error handling - **Category support** for actionable notifications ### 3. FCM Client (`internal/push/fcm.go`) Uses direct HTTP calls to the FCM legacy API (`https://fcm.googleapis.com/fcm/send`): - **Server key authentication** - **Batch sending** via `registration_ids` - **Data payload** for Android action handling ### 4. Notification Service (`internal/services/notification_service.go`) Orchestrates notification creation and delivery: ```go func (s *NotificationService) CreateAndSendTaskNotification( ctx context.Context, userID uint, notifType models.NotificationType, task *models.Task, ) error ``` **Responsibilities:** 1. Create notification record in database 2. Determine button types based on task state 3. Map button types to iOS category 4. Get user's device tokens 5. Send via PushClient ### 5. Task Button Types (`internal/services/task_button_types.go`) Determines which action buttons to show based on task state: | Task State | Button Types | |------------|--------------| | Overdue | `edit`, `complete`, `cancel`, `mark_in_progress` | | Due Soon | `edit`, `complete`, `cancel`, `mark_in_progress` | | In Progress | `edit`, `complete`, `cancel` | | Cancelled | `edit` | | Completed | `edit` | ## Scheduled Notifications The worker process (`cmd/worker/main.go`) sends scheduled notifications using Asynq: ### Schedule | Job | Default Time (UTC) | Config Variable | |-----|-------------------|-----------------| | Task Reminders | 8:00 PM | `TASK_REMINDER_HOUR`, `TASK_REMINDER_MINUTE` | | Overdue Alerts | 9:00 AM | `OVERDUE_REMINDER_HOUR` | | Daily Digest | 11:00 AM | `DAILY_DIGEST_HOUR` | ### Job Handlers (`internal/worker/jobs/handler.go`) 1. **HandleTaskReminder** - Sends actionable notifications for tasks due today/tomorrow 2. **HandleOverdueReminder** - Sends actionable notifications for overdue tasks 3. **HandleDailyDigest** - Sends summary notification with task statistics ## iOS Categories iOS actionable notifications use categories to define available actions. The API sends a `category` field that maps to categories registered in the iOS app: | Category ID | Actions | |-------------|---------| | `TASK_ACTIONABLE` | Complete, Edit, Skip, In Progress | | `TASK_IN_PROGRESS` | Complete, Edit, Cancel | | `TASK_CANCELLED` | Edit | | `TASK_COMPLETED` | Edit | The iOS app must register these categories in `AppDelegate`: ```swift let completeAction = UNNotificationAction(identifier: "COMPLETE", title: "Complete") let editAction = UNNotificationAction(identifier: "EDIT", title: "Edit") // ... more actions let taskCategory = UNNotificationCategory( identifier: "TASK_ACTIONABLE", actions: [completeAction, editAction, skipAction, inProgressAction], intentIdentifiers: [] ) ``` ## Configuration ### Environment Variables | Variable | Required | Description | |----------|----------|-------------| | `APNS_AUTH_KEY_PATH` | For iOS | Path to .p8 key file | | `APNS_AUTH_KEY_ID` | For iOS | Key ID from Apple Developer | | `APNS_TEAM_ID` | For iOS | Team ID from Apple Developer | | `APNS_TOPIC` | For iOS | Bundle ID (e.g., `com.tt.honeyDue.honeyDueDev`) | | `APNS_PRODUCTION` | No | `true` for production, `false` for sandbox | | `APNS_USE_SANDBOX` | No | Deprecated, use `APNS_PRODUCTION` | | `FCM_SERVER_KEY` | For Android | Firebase Cloud Messaging server key | ### Docker/Dokku Setup Mount the .p8 key file: ```bash # Docker volumes: - ./push_certs:/certs:ro # Dokku dokku storage:mount honeydue-api /path/to/push_certs:/certs ``` ## Data Flow ### Real-time Notification (e.g., task assigned) ``` 1. User assigns task via API 2. Handler calls NotificationService.CreateAndSendTaskNotification() 3. NotificationService: a. Creates Notification record in database b. Calls GetButtonTypesForTask() to determine actions c. Maps buttons to iOS category d. Gets user's device tokens from APNSDevice/GCMDevice tables e. Calls PushClient.SendActionableNotification() 4. PushClient sends to APNs and/or FCM 5. Device receives notification with action buttons ``` ### Scheduled Notification (e.g., daily overdue alert) ``` 1. Asynq scheduler triggers job at configured time 2. Worker calls HandleOverdueReminder() 3. Handler: a. Queries all overdue tasks with Preloads b. Groups tasks by user (assigned_to or residence owner) c. Checks user's notification preferences d. For each user with enabled notifications: - Sends up to 5 individual actionable notifications - Sends summary if more than 5 tasks 4. NotificationService creates records and sends via PushClient ``` ## Device Token Management Device tokens are stored in two tables: - `push_notifications_apnsdevice` - iOS device tokens - `push_notifications_gcmdevice` - Android device tokens (FCM) Fields: - `registration_id` - The device token - `user_id` - Associated user - `active` - Whether to send to this device - `device_id` - Unique device identifier Tokens are registered via: - `POST /api/push/apns/` - Register iOS device - `POST /api/push/gcm/` - Register Android device ## Error Handling ### APNs Errors The APNs client handles individual token failures: - Logs each failure with token prefix and reason - Continues sending to remaining tokens - Returns error only if ALL tokens fail Common APNs errors: - `BadDeviceToken` - Invalid or expired token - `Unregistered` - App uninstalled - `TopicDisallowed` - Bundle ID mismatch ### FCM Errors The FCM client parses the response to identify failed tokens: - Logs failures with error type - Returns error only if ALL tokens fail Common FCM errors: - `InvalidRegistration` - Invalid token - `NotRegistered` - App uninstalled - `MismatchSenderId` - Wrong server key ## Debugging ### Check Push Configuration ```go log.Info(). Bool("ios_enabled", pushClient.IsIOSEnabled()). Bool("android_enabled", pushClient.IsAndroidEnabled()). Msg("Push notification client initialized") ``` ### View Worker Logs ```bash # Docker docker-compose logs -f worker # Dokku dokku logs honeydue-api -p worker ``` ### Test Push Manually The API has a test endpoint (debug mode only): ```bash curl -X POST https://api.example.com/api/push/test/ \ -H "Authorization: Token " \ -H "Content-Type: application/json" \ -d '{"title": "Test", "message": "Hello"}' ``` ## Migration from Gorush The API previously used Gorush as an intermediate push server. The migration to direct APNs/FCM involved: 1. **Added** `internal/push/apns.go` - Direct APNs using apns2 library 2. **Added** `internal/push/fcm.go` - Direct FCM using HTTP 3. **Added** `internal/push/client.go` - Unified client wrapper 4. **Removed** Gorush service from docker-compose.yml 5. **Removed** `GORUSH_URL` configuration Benefits of direct approach: - No additional container/service to manage - Lower latency (one less hop) - Full control over notification payload - Simplified debugging