Nudge is a full-stack reminder platform that keeps you on track with recurring events, color-coded categories, powerful search, a habit tracker with streaks, and real-time push notifications — all wrapped in a clean, animated Flutter experience.
- Features
- Tech Stack
- Monorepo Structure
- Architecture
- Getting Started
- Running the Mobile App
- API Overview
- Database Schema
- How Notifications Work
- Quality & Verification
- Troubleshooting
Create events that repeat daily, weekly, monthly, or yearly. Nudge auto-generates future instances, keeps them topped up with a daily background job, and cascades deletes across a whole series. Editing/deleting a repeating event asks whether you mean this event only or the whole series.
Organize events with color-coded categories. Create, edit, and manage them, assign multiple to any event, and see them as colorful badges across the calendar and detail screens.
Instantly search events by title and filter by status (Active / Completed / Cancelled), event type, and category — all with a smooth, debounced search experience.
Build daily or weekly habits, check in with one tap, and watch your streak grow. Habits appear both as a dedicated screen and as a quick "Today's Habits" section on the dashboard, with optimistic UI updates.
Powered by Firebase Cloud Messaging. A resilient scheduler dispatches reminders with row-locking (SKIP LOCKED), exponential-backoff retries, and stale-lock recovery. Tapping a notification deep-links straight to the event. Runs in a console simulator mode during local dev.
Email/password and Google Sign-In (OAuth) with JWT access + refresh token rotation, session management, and secure password hashing.
Connect a Google account (even a different one from your login) to sync events to Google Calendar.
|
|
Turborepo monorepo · Vitest tests · ESLint + Prettier · npm workspaces
nudge/
├── apps/
│ ├── backend/ # Express + TypeScript REST API
│ │ └── src/
│ │ ├── modules/ # auth, events, reminders, categories,
│ │ │ # habits, notifications, dashboard
│ │ ├── scheduler/ # cron: reminder dispatch + recurrence
│ │ ├── integrations/ # firebase (FCM)
│ │ └── config/ # env, logger
│ └── user-app/ # Flutter mobile client
│ └── lib/
│ ├── screens/ # dashboard, calendar, habits, profile…
│ └── services/ # api client + FCM service
└── packages/
├── db/ # Prisma schema, migrations, generated client
├── ui/ # shared UI package
├── eslint-config/ # shared lint config
└── typescript-config/
┌─────────────────────┐ HTTPS/REST ┌──────────────────────┐
│ Flutter App │ ───────────────────────► │ Express API │
│ (Android / iOS) │ ◄─────────────────────── │ (Node + TypeScript) │
└─────────┬───────────┘ JWT auth └──────────┬───────────┘
│ │
│ FCM push Prisma ORM │
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ Firebase Cloud │ ◄───── Firebase Admin ──── │ PostgreSQL (Neon) │
│ Messaging │ │ + Redis (limits) │
└─────────────────────┘ └──────────────────────┘
▲
│ node-cron every minute
┌────────┴─────────┐
│ Reminder │ claims due reminders (SKIP LOCKED),
│ Scheduler │ dispatches FCM, retries w/ backoff,
└───────────────────┘ generates recurring instances daily
- Node.js >= 18 and npm >= 10
- Flutter SDK >= 3.0
- A PostgreSQL database (e.g. free Neon)
- A Firebase project (for push notifications)
git clone <your-repo-url> nudge
cd nudge
npm installCreate packages/db/.env:
DATABASE_URL="postgresql://user:pass@host/dbname"Create apps/backend/.env:
PORT=3000
DATABASE_URL="postgresql://user:pass@host/dbname"
CORS_ALLOWED_ORIGINS="*"
# JWT (RS256 keys)
JWT_ACCESS_PRIVATE_KEY="..."
JWT_ACCESS_PUBLIC_KEY="..."
# Google OAuth
GOOGLE_WEB_CLIENT_ID="xxx.apps.googleusercontent.com"
# Firebase Admin (from service-account JSON)
FIREBASE_PROJECT_ID="..."
FIREBASE_CLIENT_EMAIL="..."
FIREBASE_PRIVATE_KEY="..."
# Optional
REDIS_URL="redis://localhost:6379"cd packages/db
npx prisma migrate deploy # apply migrations
npx prisma generate # generate the clientcd apps/backend
npm run dev # http://localhost:3000 (listens on 0.0.0.0)Health check: GET /health/live -> { "status": "alive" }
Push notifications and Google Sign-In only work on real Android/iOS devices, not on Windows/desktop.
- Drop your Firebase
google-services.jsonintoapps/user-app/android/app/. - Set your PC's LAN IP in
apps/user-app/lib/services/api/api_client.dart:(Find it withstatic String baseUrl = 'http://<YOUR_PC_LAN_IP>:3000/api/v1';
ipconfig-> "Wireless LAN adapter Wi-Fi" IPv4.) - Connect your phone (USB debugging on), same Wi-Fi as your PC.
- Run:
cd apps/user-app flutter pub get flutter run
Connectivity test: open http://<YOUR_PC_IP>:3000/health/live in your phone's browser. If you see {"status":"alive"}, you're connected.
Base path: /api/v1
| Area | Endpoints |
|---|---|
| Auth | POST /auth/register · POST /auth/login · POST /auth/google · POST /auth/refresh · GET /auth/me · GET/DELETE /auth/sessions |
| Events | POST /events · GET /events (search, filters, pagination) · GET /events/:id · PATCH /events/:id · DELETE /events/:id · POST /events/:id/complete |
| Categories | POST /categories · GET /categories · PATCH /categories/:id · DELETE /categories/:id |
| Habits | POST /habits · GET /habits · PATCH /habits/:id · DELETE /habits/:id · POST/DELETE /habits/:id/check-in · GET /habits/:id/streak |
| Reminders | POST /events/:id/reminders · GET /events/:id/reminders · PATCH /reminders/:id · DELETE /reminders/:id |
| Notifications | PUT /notifications/device-token · GET /notifications/history · GET /notifications/health |
| Dashboard | GET /dashboard/summary · GET /dashboard/analytics |
| Health | GET /health/live · GET /health/ready · GET /health/scheduler |
Core models (Prisma + PostgreSQL):
- User — accounts, auth, timezone
- Event — with recurrence fields (
recurrenceFrequency,recurrenceInterval,recurrenceEndAt,parentEventIdself-relation) - Reminder — scheduled offsets, retry state, locking
- Category / EventCategory — categories + many-to-many join
- Habit / HabitLog — habits + daily check-ins (unique per day)
- DeviceToken / NotificationLog — FCM tokens + delivery audit
- Session / AuthAccount — refresh tokens + OAuth links
- When you create an event with reminders, Reminder rows are scheduled at your chosen offsets.
- A node-cron job runs every minute and:
- Recovers stale locks
- Claims due reminders using
SELECT ... FOR UPDATE SKIP LOCKED(safe for concurrency) - Dispatches them via Firebase Admin to all active device tokens
- Retries failures with exponential backoff; deactivates dead tokens
- A daily job tops up open-ended recurring series ~30 days ahead.
- On device: background/closed -> shown in the system tray automatically; foreground -> a themed in-app banner; tapping -> deep-links to the event.
Without Firebase credentials, the backend logs a mock push to the console (simulator mode) — perfect for local dev.
| Check | Result |
|---|---|
flutter analyze |
0 issues |
Backend tsc --noEmit |
0 errors |
| Android debug APK build | Builds successfully |
Prisma migrate status |
In sync, no drift |
# Frontend
cd apps/user-app && flutter analyze
# Backend
cd apps/backend && npx tsc --noEmit && npm testApp loads but no data (Connection timed out)
- Your
baseUrlmust be your PC's LAN IP, not127.0.0.1(that means the phone itself). - Allow port 3000 through Windows Firewall (Admin PowerShell):
New-NetFirewallRule -DisplayName "Nudge Backend 3000" -Direction Inbound -LocalPort 3000 -Protocol TCP -Action Allow
- Use a normal Wi-Fi router, not a mobile hotspot (hotspots often block phone<->PC).
- Test in the phone browser:
http://<YOUR_PC_IP>:3000/health/live.
Google login shows a "test user" instead of my Gmail
- Real Google Sign-In only works on a physical device (not Windows/emulator).
- Ensure your debug SHA-1 is registered in Firebase and
google-services.jsonis up to date. - If your OAuth consent screen is in Testing mode, add your Gmail under Test users.
Android build fails
- Make sure
google-services.jsonis inapps/user-app/android/app/. - Cleartext HTTP is enabled for LAN dev via
usesCleartextTraffic="true"in the manifest. - Run
flutter clean && flutter pub getand rebuild.