Files
honeyDueAPI/docs/PUSH_NOTIFICATIONS.md
Trey t 5a6bad3ec3 Remove Gorush, use direct APNs/FCM, fix worker queries
- 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>
2025-12-06 00:59:42 -06:00

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

  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:

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

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:

  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