Tasks and notes that stay in sync on every device, online or offline.
A full-stack, offline-first app with real-time sync, due dates, local reminders, and a fully bilingual (English / Persian, RTL) interface.
Features · Architecture · Getting started · Design decisions · Roadmap
SyncNotes is a task and note app I built end to end: a NestJS API with PostgreSQL and a React Native app for iOS and Android. Changes made on one device show up on the others within moments, and the app keeps working without a connection. Anything you do offline is queued and sent when the network returns.
I built it to show how I approach a real product problem across the whole stack: data modelling, authentication, real-time communication, offline behaviour, internationalisation, and shipping an installable build.
| Area | What it does |
|---|---|
| Tasks and notes | Create, edit, and delete tasks and notes with tags, full-text search, and filters. |
| Due dates and reminders | Set a due date on a task and pick a reminder (on time, 1 hour before, or 1 day before). Reminders are local notifications, so they work offline. Overdue tasks are highlighted in the list. |
| Real-time sync | Changes are pushed to your other devices over WebSocket (Socket.IO), scoped to your account. |
| Offline-first | Edits apply instantly on the device and are queued in an outbox. The queue is sent in order when you are back online. |
| Authentication | Email and password sign-up and sign-in, short-lived access tokens, rotating refresh tokens, and secure on-device token storage. |
| Bilingual UI | English and Persian with i18next. Persian uses a right-to-left layout. The app starts in the device language. |
| Themes | Light, dark, or follow the system. |
| Sync status | A small status pill always shows whether you are live, offline, syncing, or have changes waiting to sync. |
| Accessibility | Labelled controls, large touch targets, and screen-reader announcements for status changes. |
flowchart LR
subgraph Mobile["Mobile app: React Native + Expo"]
UI["Screens (Expo Router)"]
Store["Zustand stores"]
Outbox["Offline outbox"]
Engine["Sync engine"]
UI --> Store
Store --> Outbox
Outbox --> Engine
end
subgraph Server["Backend: NestJS"]
API["Controllers and guards"]
Services["Services"]
Gateway["Realtime gateway"]
ORM["Prisma ORM"]
API --> Services
Services --> ORM
Services --> Gateway
end
DB[("PostgreSQL")]
Engine -->|"REST + JWT"| API
Engine <-->|"WebSocket"| Gateway
ORM --> DB
sequenceDiagram
participant U as User
participant A as App (local store)
participant S as Server
participant O as Other devices
U->>A: Edit a task
A->>A: Apply instantly and queue in the outbox
A->>S: Send the queued change when online
S->>S: Validate, save, bump updatedAt
S-->>A: Return the saved item
S-->>O: Push the change over WebSocket
| Layer | Technology |
|---|---|
| Mobile | React Native, Expo (SDK 57), Expo Router, TypeScript |
| State | Zustand with persisted stores |
| Internationalisation | i18next, react-i18next, expo-localization |
| Mobile storage | expo-secure-store (tokens), AsyncStorage (items and outbox) |
| Notifications | expo-notifications (local), @react-native-community/datetimepicker |
| Backend | NestJS 11, TypeScript |
| Database | PostgreSQL, Prisma |
| Real time | Socket.IO |
| Security | JWT, bcrypt, helmet, rate limiting, request validation |
syncnotes/
├── backend/ NestJS API, Prisma schema, WebSocket gateway
├── mobile/ React Native app (Expo)
└── docs/ Images used in the READMEs
Each part has its own README with more detail:
backend/README.md: API reference, data model, security, deploymentmobile/README.md: app architecture, offline sync, notifications, building an installable app
- Node.js 20 or newer
- Docker (for a local PostgreSQL), or any other PostgreSQL instance
- A phone with Expo Go, or an Android/iOS emulator
cd backend
cp .env.example .env # then set the two JWT secrets
docker compose up -d # PostgreSQL
npm install
npm run prisma:migrate # create the tables
npm run start:dev # http://localhost:3000/apicd mobile
cp .env.example .env # set EXPO_PUBLIC_API_URL
npm install
npx expo startSet EXPO_PUBLIC_API_URL to an address your phone can reach, for example http://192.168.1.20:3000 on the same Wi-Fi, or http://10.0.2.2:3000 from the Android emulator. Do not add /api.
Reminders use native notifications, which Expo Go does not support on Android. Everything else works in Expo Go. To test reminders on Android, build a development build or the APK (see the mobile README).
- Offline-first with an outbox. Every change is applied locally first and recorded as an operation in a persisted queue. Edits to the same item are merged, and a create followed by a delete cancels out before it ever reaches the server. A revision counter protects edits that happen while a request is in flight.
- Client-generated UUIDs. The app creates item IDs itself, so a create can be retried safely. A duplicate returns
409and the client treats it as already saved. - Soft deletes plus
updatedSince. Deleted items keep adeletedAttimestamp, so a device that was offline can ask for everything that changed since its last sync, including deletions. - Refresh token rotation. Each refresh issues a new pair. Only a SHA-256 hash of the refresh token is stored, and reusing an old token revokes the session. I used SHA-256 rather than bcrypt here because bcrypt only reads the first 72 bytes, and the start of every JWT looks the same.
- Local reminders. Notifications are scheduled on the device from synced data, so they work offline and need no push service. The schedule is rebuilt whenever tasks or the language change. iOS keeps at most 64 pending notifications, so the nearest 60 are scheduled.
- Last write wins. If a device has an unsent local change for an item, that local version takes priority over an incoming server version. It is simple and predictable for a single-user app.
- Authentication with refresh token rotation
- Real-time sync over WebSocket
- Offline-first outbox
- Due dates and local reminders
- English and Persian with RTL
- Automated tests (unit and end to end)
- Server push notifications (FCM and APNs) so reminders reach every device
- Persian (Jalali) calendar picker
- Docker image and CI pipeline for the backend
- Web client sharing the same API
Built by Mobin, a full-stack developer working with Next.js, NestJS, React Native, and PostgreSQL.