# FCM (Firebase Cloud Messaging) Implementation Guide ## Overview This document describes the complete FCM push notification implementation for Sojorn, covering both the Go backend and Flutter client. The system supports Android, iOS, and Web platforms. --- ## Architecture ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Flutter App │────▶│ Go Backend │────▶│ Firebase FCM │ │ (Android/Web) │ │ Push Service │ │ Cloud Messaging │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ │ POST /notifications/device │ │ (Register FCM Token) │ │ │ │ │ │ SendPush() │ │ │──────────────────────▶│ │ │ │ │◀──────────────────────│◀──────────────────────│ │ Push Notification │ │ ``` --- ## Backend Configuration ### Required Environment Variables ```env # Firebase Cloud Messaging (FCM) FIREBASE_CREDENTIALS_FILE=/opt/sojorn/firebase-service-account.json FIREBASE_WEB_VAPID_KEY=BNxS7_your_actual_vapid_key_here ``` ### Firebase Service Account JSON Download from Firebase Console > Project Settings > Service Accounts > Generate New Private Key The JSON file should contain: - `project_id` - `private_key` - `client_email` --- ## Database Schema ### `user_fcm_tokens` Table ```sql CREATE TABLE IF NOT EXISTS user_fcm_tokens ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, token TEXT NOT NULL, device_type TEXT, -- 'web', 'android', 'ios', 'desktop' last_updated TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(user_id, token) -- Prevents duplicate tokens per device ); ``` **Key Features:** - Composite unique constraint on `(user_id, token)` prevents duplicate registrations - `device_type` column supports platform-specific logic - `ON DELETE CASCADE` ensures tokens are cleaned up when user is deleted --- ## Push Notification Triggers | Event | Handler | Recipient | Data Payload | |-------|---------|-----------|--------------| | New Follower | `user_handler.go:Follow` | Followed user | `type`, `follower_id` | | Follow Request | `user_handler.go:Follow` | Target user | `type`, `follower_id` | | Follow Accepted | `user_handler.go:AcceptFollowRequest` | Requester | `type`, `follower_id` | | New Chat Message | `chat_handler.go:SendMessage` | Receiver | `type`, `conversation_id`, `encrypted` | | Comment on Post | `post_handler.go:CreateComment` | Post author | `type`, `post_id`, `post_type`, `target` | | Post Saved | `post_handler.go:SavePost` | Post author | `type`, `post_id`, `post_type`, `target` | | Beacon Vouched | `post_handler.go:VouchBeacon` | Beacon author | `type`, `beacon_id`, `target` | | Beacon Reported | `post_handler.go:ReportBeacon` | Beacon author | `type`, `beacon_id`, `target` | --- ## Data Payload Structure All push notifications include a `data` payload for deep linking: ```json { "type": "comment|save|reply|chat|new_follower|follow_request|beacon_vouch", "post_id": "uuid", // For post-related notifications "conversation_id": "uuid", // For chat notifications "follower_id": "uuid", // For follow notifications "beacon_id": "uuid", // For beacon notifications "target": "main_feed|quip_feed|beacon_map|secure_chat|profile|thread_view" } ``` --- ## Flutter Client Implementation ### Initialization Flow 1. **Android 13+ Permission Check**: Explicitly request `POST_NOTIFICATIONS` permission 2. **Firebase Permission Request**: Request FCM permissions via SDK 3. **Token Retrieval**: Get FCM token (with VAPID key for web) 4. **Backend Registration**: POST token to `/notifications/device` 5. **Set up Listeners**: Handle token refresh and message callbacks ### Deep Linking (Message Open Handling) The Flutter client handles these notification types: | Type | Navigation Target | |------|-------------------| | `chat`, `new_message` | SecureChatScreen with conversation | | `save`, `comment`, `reply` | Based on `target` field (home/quips/beacon) | | `new_follower`, `follow_request` | Profile screen of follower | | `beacon_vouch`, `beacon_report` | Beacon map | ### Logout Flow On logout, the client: 1. Calls `DELETE /notifications/device` with the current token 2. Deletes the token from Firebase locally 3. Resets initialization state --- ## API Endpoints ### Register Device Token ```http POST /api/v1/notifications/device Authorization: Bearer Content-Type: application/json { "fcm_token": "device_token_here", "platform": "android|ios|web|desktop" } ``` ### Unregister Device Token ```http DELETE /api/v1/notifications/device Authorization: Bearer Content-Type: application/json { "fcm_token": "device_token_here" } ``` --- ## Troubleshooting ### Token Not Registering 1. Check Firebase is initialized properly 2. Verify `FIREBASE_CREDENTIALS_FILE` path is correct 3. For web, ensure `FIREBASE_WEB_VAPID_KEY` is set 4. Check network connectivity to Go backend ### Notifications Not Arriving 1. Verify token exists in `user_fcm_tokens` table 2. Check Firebase Console for delivery reports 3. Ensure app hasn't restricted background data 4. On Android 13+, verify POST_NOTIFICATIONS permission ### Invalid Token Cleanup The `PushService` automatically removes invalid tokens when FCM returns `messaging.IsRegistrationTokenNotRegistered` error. --- ## Testing ### Manual Token Verification ```sql SELECT user_id, token, device_type, last_updated FROM user_fcm_tokens WHERE user_id = 'your-user-uuid'; ``` ### Send Test Notification Use Firebase Console > Cloud Messaging > Send test message with the device token. --- ## Platform-Specific Notes ### Android - Target SDK 33+ requires `POST_NOTIFICATIONS` runtime permission - Add to `AndroidManifest.xml`: ```xml ``` ### Web - Requires valid VAPID key from Firebase Console - Service worker must be properly configured - HTTPS required (except localhost) ### iOS - Requires APNs configuration in Firebase - Provisioning profile must include push notification capability