- Remove Gorush push server dependency (now using direct APNs/FCM) - Update docker-compose.yml to remove gorush service - Update config.go to remove GORUSH_URL - Fix worker queries: - Use auth_user instead of user_user table - Use completed_at instead of completion_date column - Add NotificationService to worker handler for actionable notifications - Add docs/PUSH_NOTIFICATIONS.md with architecture documentation - Update README.md, DOKKU_SETUP.md, and dev.sh 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
10 KiB
Push Notifications Architecture
This document describes how push notifications work in the Casera API.
Overview
The Casera 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
┌─────────────────────────────────────────────────────────────────────────┐
│ Casera 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:
type Client struct {
apns *APNsClient // iOS
fcm *FCMClient // Android
}
Key Methods:
SendToIOS()- Send to iOS devicesSendToAndroid()- Send to Android devicesSendToAll()- Send to both platformsSendActionableNotification()- Send with iOS category for action buttons
2. APNs Client (internal/push/apns.go)
Uses the 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:
func (s *NotificationService) CreateAndSendTaskNotification(
ctx context.Context,
userID uint,
notifType models.NotificationType,
task *models.Task,
) error
Responsibilities:
- Create notification record in database
- Determine button types based on task state
- Map button types to iOS category
- Get user's device tokens
- 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)
- HandleTaskReminder - Sends actionable notifications for tasks due today/tomorrow
- HandleOverdueReminder - Sends actionable notifications for overdue tasks
- 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:
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.casera.CaseraDev) |
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:
# Docker
volumes:
- ./push_certs:/certs:ro
# Dokku
dokku storage:mount casera-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 tokenspush_notifications_gcmdevice- Android device tokens (FCM)
Fields:
registration_id- The device tokenuser_id- Associated useractive- Whether to send to this devicedevice_id- Unique device identifier
Tokens are registered via:
POST /api/push/apns/- Register iOS devicePOST /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 tokenUnregistered- App uninstalledTopicDisallowed- 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 tokenNotRegistered- App uninstalledMismatchSenderId- Wrong server key
Debugging
Check Push Configuration
log.Info().
Bool("ios_enabled", pushClient.IsIOSEnabled()).
Bool("android_enabled", pushClient.IsAndroidEnabled()).
Msg("Push notification client initialized")
View Worker Logs
# Docker
docker-compose logs -f worker
# Dokku
dokku logs casera-api -p worker
Test Push Manually
The API has a test endpoint (debug mode only):
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:
- Added
internal/push/apns.go- Direct APNs using apns2 library - Added
internal/push/fcm.go- Direct FCM using HTTP - Added
internal/push/client.go- Unified client wrapper - Removed Gorush service from docker-compose.yml
- Removed
GORUSH_URLconfiguration
Benefits of direct approach:
- No additional container/service to manage
- Lower latency (one less hop)
- Full control over notification payload
- Simplified debugging