Files
honeyDueAPI/docs/PUSH_NOTIFICATIONS.md
Trey t 4976eafc6c Rebrand from Casera/MyCrib to honeyDue
Total rebrand across all Go API source files:
- Go module path: casera-api -> honeydue-api
- All imports updated (130+ files)
- Docker: containers, images, networks renamed
- Email templates: support email, noreply, icon URL
- Domains: casera.app/mycrib.treytartt.com -> honeyDue.treytartt.com
- Bundle IDs: com.tt.casera -> com.tt.honeyDue
- IAP product IDs updated
- Landing page, admin panel, config defaults
- Seeds, CI workflows, Makefile, docs
- Database table names preserved (no migration needed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:33:38 -06:00

295 lines
10 KiB
Markdown

# 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 <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