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>
295 lines
10 KiB
Markdown
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
|