11 KiB
Daily Reminder Feature - Complete Setup & Context
Date: 2026-04-20
Status: ✅ Enabled & Ready for Testing
Overview
The Daily Reminder feature is a fully implemented Firebase Cloud Messaging (FCM) system that sends push notifications to remind users to journal. It works even when the browser is closed (on mobile PWA).
Key Point: All code was already in place but disabled in the UI. This document captures the setup and what was changed to enable it.
Architecture
Frontend Flow
Files: src/hooks/useReminder.ts, src/hooks/reminderApi.ts, src/pages/SettingsPage.tsx
- User opens Settings → clicks "Daily Reminder" button
- Modal opens with time picker (
ClockTimePickercomponent) - User selects time (e.g., 08:00) → clicks "Save"
enableReminder()is called:- Requests browser notification permission (
Notification.requestPermission()) - Gets FCM token from service worker
- Sends token to backend:
POST /api/notifications/fcm-token - Sends settings to backend:
PUT /api/notifications/reminder/{userId} - Stores time + enabled state in localStorage
- Requests browser notification permission (
Message Handling:
listenForegroundMessages()called on app mount (insrc/main.tsx)- When app is focused: Firebase SDK triggers
onMessage()→ shows notification manually - When app is closed: Service worker (
public/sw.js) handles it viaonBackgroundMessage()→ shows notification
Backend Flow
Files: backend/scheduler.py, backend/routers/notifications.py, backend/main.py
Initialization:
start_scheduler()called in FastAPI app lifespan- Initializes Firebase Admin SDK (requires
FIREBASE_SERVICE_ACCOUNT_JSON) - Starts APScheduler cron job
Every Minute:
- Find all users with
reminder.enabled=trueand FCM tokens - For each user:
- Convert UTC time → user's timezone (stored in DB)
- Check if current HH:MM matches
reminder.time(e.g., "08:00") - Check if already notified today (via
reminder.lastNotifiedDate) - Check if user has written a journal entry today
- If NOT written yet: Send FCM push via
firebase_admin.messaging.send_each_for_multicast() - Auto-prune stale tokens on failure
- Mark as notified today
Database Structure (MongoDB):
users collection {
_id: ObjectId,
fcmTokens: [token1, token2, ...], // per device
reminder: {
enabled: boolean,
time: "HH:MM", // 24-hour format
timezone: "Asia/Kolkata", // IANA timezone
lastNotifiedDate: "2026-04-16" // prevents duplicates today
}
}
Changes Made (2026-04-20)
1. Updated Frontend Environment (.env.local)
Changed: Firebase credentials from mentor's project → personal test project
VITE_FIREBASE_API_KEY=AIzaSyAjGq7EFrp1mE_8Ni2iZz8LNk7ySVz-lX8
VITE_FIREBASE_AUTH_DOMAIN=react-test-8cb04.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=react-test-8cb04
VITE_FIREBASE_MESSAGING_SENDER_ID=1036594341832
VITE_FIREBASE_APP_ID=1:1036594341832:web:9db6fa337e9cd2e953c2fd
VITE_FIREBASE_VAPID_KEY=BLXhAWY-ms-ACW4PFpqnPak3VZobBIruylVE8Jt-Gm4x53g4aAzEhQzjTvGW8O7dX76-ZoUjlBV15b-EODr1IaY
2. Updated Backend Environment (backend/.env)
Changed: Added Firebase service account JSON (from personal test project)
FIREBASE_SERVICE_ACCOUNT_JSON={"type":"service_account","project_id":"react-test-8cb04",...}
3. Deleted Service Account JSON File
- Removed:
service account.json(no longer needed — credentials now in env var)
4. Enabled Reminder UI (src/pages/SettingsPage.tsx)
Before:
<div className="settings-item" style={{ opacity: 0.5 }}>
<label className="settings-toggle">
<input type="checkbox" checked={false} disabled readOnly />
</label>
</div>
After:
<button
type="button"
className="settings-item settings-item-button"
onClick={handleOpenReminderModal}
>
<div className="settings-item-content">
<h4 className="settings-item-title">Daily Reminder</h4>
<p className="settings-item-subtitle">
{reminderEnabled && reminderTime
? `Set for ${reminderTime}`
: "Set a daily reminder"}
</p>
</div>
</button>
- Changed from disabled toggle → interactive button
- Shows current reminder time or "Set a daily reminder"
- Clicking opens time picker modal
5. Removed Type Ignore Comment
Before:
// @ts-ignore — intentionally unused, reminder is disabled (coming soon)
const handleReminderToggle = async () => {
After:
const handleReminderToggle = async () => {
Critical Code Files
| File | Purpose |
|---|---|
src/hooks/useReminder.ts |
enableReminder(), disableReminder(), reenableReminder(), getFcmToken(), listenForegroundMessages() |
src/hooks/reminderApi.ts |
saveFcmToken(), saveReminderSettings() |
backend/scheduler.py |
send_reminder_notifications(), _process_user(), _send_push(), init_firebase() |
backend/routers/notifications.py |
POST /fcm-token, PUT /reminder/{user_id} endpoints |
public/sw.js |
Service worker background message handler |
src/pages/SettingsPage.tsx |
UI: time picker modal, reminder state mgmt |
src/main.tsx |
Calls listenForegroundMessages() on mount |
backend/main.py |
Scheduler initialization in app lifespan |
How to Test
Prerequisites
- ✅ Backend
.envhas Firebase service account JSON - ✅ Frontend
.env.localhas Firebase web config + VAPID key - ✅ UI is enabled (button visible in Settings)
Steps
-
Restart the backend (so it picks up new
FIREBASE_SERVICE_ACCOUNT_JSON)docker-compose down docker-compose up -
Open the app and go to Settings
-
Click "Daily Reminder" → time picker modal opens
-
Pick a time (e.g., 14:30 for testing: pick a time 1-2 minutes in the future)
-
Click "Save"
- Browser asks for notification permission → Accept
- Time is saved locally + sent to backend
-
Monitor backend logs:
docker logs grateful-journal-backend-1 -fLook for:
Reminder sent to user {user_id}: X ok, 0 failed -
At the reminder time:
- If browser is open: notification appears in-app
- If browser is closed: PWA/OS notification appears (mobile)
Troubleshooting
| Issue | Solution |
|---|---|
| Browser asks for notification permission repeatedly | Check Notification.permission === 'default' in browser console |
| FCM token is null | Check VITE_FIREBASE_VAPID_KEY is correct; browser may not support FCM |
| Scheduler doesn't run | Restart backend; check FIREBASE_SERVICE_ACCOUNT_JSON is valid JSON |
| Notification doesn't appear | Check reminder.lastNotifiedDate in MongoDB; trigger time must match exactly |
| Token registration fails | Check backend logs; 400 error means invalid userId format (must be valid ObjectId) |
Environment Variables Reference
Frontend (.env.local)
VITE_FIREBASE_API_KEY # Firebase API key
VITE_FIREBASE_AUTH_DOMAIN # Firebase auth domain
VITE_FIREBASE_PROJECT_ID # Firebase project ID
VITE_FIREBASE_MESSAGING_SENDER_ID # Firebase sender ID
VITE_FIREBASE_APP_ID # Firebase app ID
VITE_FIREBASE_VAPID_KEY # FCM Web Push VAPID key (from Firebase Console → Messaging)
VITE_API_URL # Backend API URL (e.g., http://localhost:8001/api)
Backend (backend/.env)
FIREBASE_SERVICE_ACCOUNT_JSON # Entire Firebase service account JSON (minified single line)
MONGODB_URI # MongoDB connection string
MONGODB_DB_NAME # Database name
API_PORT # Backend port
ENVIRONMENT # production/development
FRONTEND_URL # Frontend URL for CORS
Next Steps
For Production
- Switch back to mentor's Firebase credentials (remove personal test project)
- Update
.env.localandbackend/.envwith production Firebase values
Future Improvements
- Add UI toggle to enable/disable without removing settings
- Show timezone in Settings (currently auto-detected)
- Show last notification date in UI
- Add snooze button to notifications
- Let users set multiple reminder times
Resetting to Disabled State
If you need to disable reminders again:
- Revert
.env.localandbackend/.envto mentor's credentials - Revert
src/pages/SettingsPage.tsxto show "Coming soon" UI - Add back
@ts-ignorecomment
Technical Notes
Why This Approach?
- FCM: Works on web, mobile, PWA; no polling needed
- Service Worker: Handles background notifications even when browser closed
- Timezone: Stores user's IANA timezone to support global users
- Duplicate Prevention: Tracks
lastNotifiedDateper user - Smart Timing: Only notifies if user hasn't written today (no spam)
Security Considerations
- Firebase service account JSON should never be in git (only in env vars)
- FCM tokens are device-specific; backend stores them securely
- All reminder data is encrypted end-to-end (matches app's crypto design)
Known Limitations
- Reminder check runs every minute (not more frequent)
- FCM token refresh is handled by Firebase SDK automatically
- Stale tokens are auto-pruned on failed sends
- Timezone must be valid IANA format (not GMT±X)
Quick Reference Commands
Check backend scheduler logs:
docker logs grateful-journal-backend-1 -f | grep -i "reminder\|firebase"
View user reminders in MongoDB:
docker exec grateful-journal-mongo-1 mongosh grateful_journal --eval "db.users.findOne({_id: ObjectId('...')})" --username admin --password internvps
Clear FCM tokens for a user (testing):
docker exec grateful-journal-mongo-1 mongosh grateful_journal --eval "db.users.updateOne({_id: ObjectId('...')}, {\$set: {fcmTokens: []}})" --username admin --password internvps
Support
For questions about:
- Reminders: Check daily_reminder_feature.md in memory
- FCM: Firebase Cloud Messaging docs
- APScheduler: APScheduler documentation
- Firebase Admin SDK: Firebase Admin SDK for Python docs