After months of building, I just shipped Thinkora — an all-in-one
productivity app for Android. Here's the technical breakdown of what I
built, the stack I chose, and the lessons I learned along the way.
The Problem
I was juggling 5 different apps to stay organized:
- Notion for notes
- Todoist for tasks
- CamScanner for documents
- Forest for focus
- Google Keep for quick notes
Each app did one thing well, but nothing was integrated. And worse —
most of them broke the moment I lost internet.
So I built Thinkora: one offline-first app that does it all.
What's Inside
- 📝 Rich notes with sketches, voice memos, and attachments
- ✅ Tasks with AI-generated subtasks
- 📷 Document scanner with OCR in 5 languages
- 📅 Time blocking with a draggable daily timeline
- ☁️ Cloud sync across devices
- 🍅 Pomodoro timer + habit tracker + mood journal
- 🤖 Built-in AI assistant (no API key required)
The Stack
| Layer | Choice | Why |
|---|---|---|
| Framework | React Native 0.81 | Single codebase, fast iteration |
| Storage | WatermelonDB | Offline-first, JSI-powered SQLite |
| Backend | Supabase | Auth, cloud sync, edge functions |
| AI | Gemini via Supabase Edge Functions | Free for users, secure proxy |
| OCR | Google ML Kit | On-device, 5 language scripts |
| Notifications | Notifee | Native Android channels & alarms |
| Navigation | React Navigation | Stack + tabs with custom UI |
Architectural Decisions
1. Offline-First Storage with WatermelonDB
I started with AsyncStorage but quickly hit performance walls with
1000+ notes. Switching to WatermelonDB (SQLite via JSI) was a
game-changer:
typescript
async function setNotes(notes: Note[]): Promise<void> {
await database.write(async () => {
const existing = await notesCollection.query().fetch();
const existingMap = new Map(existing.map((r) => [r.id, r]));
const incomingIds = new Set(notes.map((n) => n.id));
const ops: any[] = [];
// Atomic batch operation: delete, update, create
for (const row of existing) {
if (!incomingIds.has(row.id)) ops.push(row.prepareDestroyPermanently());
}
for (const note of notes) {
const row = existingMap.get(note.id);
if (row) {
ops.push(row.prepareUpdate((r) => { /* ... */ }));
} else {
ops.push(notesCollection.prepareCreate((r) => { /* ... */ }));
}
}
await database.batch(...ops);
});
}
Try It Out
If any of this resonates, I'd love your feedback:
📲 Free on Play Store: https://play.google.com/store/apps/details?id=com.thinkora












