Notes that Think - AI powered Notes App
- Aayushi Srivastava
- Mar 11
- 9 min read
Updated: Apr 23
I built an AI-powered notes app to fix what Apple Notes doesn’t do well.
It’s great for storing notes—but not for organizing or understanding them.
So I built one that:
Auto-organizes notes by intent, not folders
Cleans up messy thoughts automatically
Lets you search by meaning, not keywords
From passive storage → to an active thinking assistant.
Key Insight: Raw AI is powerful. Production AI needs scaffolding.
I went in thinking:
“Give it a prompt → get structured JSON → ship feature.”
Reality:
It ignores formats
Wraps outputs in code fences
Adds explanations you didn’t ask for
Sometimes just… does something else entirely
What worked instead:
Treating outputs as untrusted input
Building multi-layer parsers (not just JSON.parse)
Designing fallbacks for every AI call
Using simpler response formats (e.g., index|text over JSON)
The shift: Stop designing for ideal AI behavior. Start designing for failure.
That’s when things actually became reliable.
======================================================================
Step- by-Step Guide for. building the App from Scratch
# Building an AI-Powered Notes App with Expo + Claude + Supabase
A complete guide to building an intelligent notes organizer app from scratch — everything we installed, configured, built, the errors we hit, and how we fixed them.
## What This App Does
An AI-powered personal notes app that:
- **Auto-categorizes** notes into broad life categories (Travel, Technology, Career, Health, etc.)
- **Auto-formats** messy text into clean, organized notes
- **Semantic search** — find notes by meaning, not just keywords ("yogurt dish" finds "dahi recipe")
- **Smart split** — detects when one note covers multiple topics and splits it
- **Duplicate merging** — finds similar notes and merges them
- **Cloud sync** — real-time sync across devices via Supabase
- **Search highlighting** — shows matched sections in search results, highlights inside notes with prev/next navigation
- **Batch organize** — select multiple notes and organize them all at once
- **Context-aware categorization** — "medicines for Canada trip" → Travel, not Health
## Tech Stack
| Component | Technology | Purpose |
|-----------|-----------|---------|
| Framework | React Native + Expo SDK 54 | Cross-platform mobile app |
| Routing | Expo Router v6 | File-based navigation |
| AI | Claude API (Haiku 4.5) | Categorization, formatting, search, split, merge |
| Cloud DB | Supabase (PostgreSQL) | Real-time sync across devices |
| Local Storage | AsyncStorage | Offline-first cache |
| Language | TypeScript | Type safety |
## Step 1: Initial Setup
### Install prerequisites
```bash
# Install Node.js (v18+) — use nvm or download from nodejs.org
node --version
# Install Expo CLI
npm install -g expo-cli
# Install EAS CLI (for building standalone apps)
npm install -g eas-cli
```
### Create the project
```bash
npx create-expo-app MyApp --template tabs
cd MyApp
```
### Install dependencies
```bash
# Core dependencies (most come with the template)
npx expo install @react-native-async-storage/async-storage
npx expo install @supabase/supabase-js
npx expo install expo-clipboard
npx expo install react-native-safe-area-context
```
### Run the app
```bash
npx expo start
# Scan QR code with Expo Go on your phone
# Phone and laptop must be on the same WiFi network
```
## Step 2: Supabase Setup (Cloud Sync)
### Create Supabase project
1. Go to [supabase.com](https://supabase.com) and create a free account
2. Create a new project
3. Go to **SQL Editor** and run this to create the notes table:
```sql
CREATE TABLE notes (
id TEXT PRIMARY KEY,
sync_id TEXT NOT NULL,
title TEXT DEFAULT '',
content TEXT DEFAULT '',
category TEXT DEFAULT '',
category_color TEXT DEFAULT '#6b7280',
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
is_processing BOOLEAN DEFAULT false
);
-- Index for fast sync queries
CREATE INDEX idx_notes_sync_id ON notes(sync_id);
-- Enable real-time subscriptions
ALTER PUBLICATION supabase_realtime ADD TABLE notes;
```
4. Get your **Project URL** and **Anon Key** from Settings > API
### Supabase service file: `services/supabase.ts`
- Creates a Supabase client with your URL and anon key
- Generates a random `sync_id` per device, stored in AsyncStorage
- Provides `fetchNotes()`, `upsertNote()`, `deleteNote()`, `subscribeToNotes()`
- `subscribeToNotes()` uses Supabase real-time to listen for changes
- Sync codes: users can copy their `sync_id` to another device to sync notes
---
## Step 3: Claude API Setup (AI Features)
### Get an API key
1. Go to [console.anthropic.com](https://console.anthropic.com)
2. Create an account and add billing
3. Go to API Keys and create a new key
4. Enter the key in the app's Settings screen
### AI service file: `services/ai.ts`
The AI service makes direct HTTP calls to `https://api.anthropic.com/v1/messages`. Key design decisions:
**Model choice**: Claude Haiku 4.5 (`claude-haiku-4-5-20251001`) — fast and cheap, good enough for note processing
**Timeout**: 30-second AbortController timeout to prevent hanging requests:
```typescript
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);
// ... fetch with signal: controller.signal
```
**CORS limitation**: Claude API blocks browser requests (CORS). AI only works on native (phone), not web. The app checks `Platform.OS === 'web'` and disables AI features in browser.
### AI functions:
| Function | Purpose | Prompt Strategy |
|----------|---------|----------------|
| `aiCategorize()` | Assign category + color | Context-aware: passes existing categories WITH sample note titles so AI understands what each category contains |
| `aiFormat()` | Clean up spelling, grammar, formatting | Returns `{title, content}` — generates title if missing |
| `aiSearch()` | Semantic search across all notes | Uses simple indices `[0], [1]` instead of real IDs. Returns `number\|snippet` format |
| `aiShouldSplit()` | Check if note covers multiple topics | Returns `{shouldSplit, parts[]}` |
| `aiFindSimilar()` | Find duplicate/similar note groups | Returns groups of note IDs to merge |
| `aiMergeNotes()` | Combine similar notes into one | Keeps all unique info, removes duplicates |
## Step 4: Key Architecture Decisions
### Local-first with cloud sync
- Notes save to AsyncStorage immediately (instant, offline)
- Then upsert to Supabase in background
- On startup: load local cache first, then fetch from Supabase
- Real-time subscription keeps notes in sync across devices
### AI processing is debounced
- When you save a note, AI processing is scheduled after 10 seconds of inactivity
- This prevents API spam while you're still typing
- Note shows "AI is analyzing..." spinner during processing
### Context-based state sharing
- Search query and snippets are shared via React Context (not URL params)
- Expo Router's typed routes make URL params difficult for dynamic data
- Context approach: `activeSearchQuery`, `activeSnippets` in NotesContext
## Step 5: The AI Prompts (What Actually Works)
### Categorization Prompt (Context-Aware)
This was iterated many times. Key learnings:
```
Categorize based on PRIMARY INTENT and CONTEXT, not surface-level keywords.
- "Daily medications" → Health
- "Medicines for Canada trip" → Travel
- "Best restaurants in Tokyo" → Travel (trip planning, not Food)
Rule: If the note is tied to a specific activity, event, or goal,
categorize under that context.
```
**Critical**: Pass existing categories WITH sample note titles:
```
Existing categories (with sample notes):
- Travel: Canada Trip Packing List, Tokyo Itinerary
- Technology: App Development Lessons, API Notes
```
Without this context, AI creates too many narrow categories or puts everything in "Other".
### Search Prompt (Semantic)
```
Perform comprehensive search beyond keyword matching.
Use semantic understanding, synonyms, contextual meaning.
Incorporate multilingual translations.
When in doubt, INCLUDE the note.
Respond with: number|exact snippet from note
```
**Key**: Use simple numeric indices `[0], [1]` not real IDs. Claude gets confused by timestamp IDs like `1712345678901`.
### Format/Clean Prompt
```
You are a professional editor. You MUST actively rewrite — never return unchanged.
Fix spelling, grammar, capitalize properly, organize into sections,
format lists consistently, remove redundancy.
Return ONLY valid JSON: {"title": "...", "content": "..."}
```
---
## Step 6: Errors We Hit and How We Fixed Them
### Error 1: AI returns 0 search results despite API working
**Root cause**: Claude Haiku ignores "respond with ONLY JSON" instructions. It wraps JSON in code fences, adds explanatory text, or returns completely different formats.
**Fix**: Multiple parsing strategies in every AI function:
```typescript
// 1. Try code fence extraction
const fenceMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
// 2. Try finding JSON object in response
const jsonMatch = raw.match(/\{[^}]*"category"[^}]*\}/);
// 3. Try cleaning and parsing directly
const cleaned = raw.replace(/```json\n?|```/g, '').trim();
```
For search, we use pipe format (`0|snippet text`) instead of JSON — it's harder for Claude to mess up.
### Error 2: 12 out of 15 notes categorized as "Other"
**Root cause**: Same JSON parsing issue — when parse fails, catch block returns `{category: 'Other'}`.
**Fix**: Robust parser + better categorization prompt with examples + passing existing category context.
### Error 3: Expo not picking up code changes
**Root cause**: Multiple Expo/Metro instances running simultaneously. Phone connects to stale instance.
**Fix**:
```bash
# Kill ALL expo processes
pkill -f expo
pkill -f metro
# Restart with cache clear
npx expo start -c
# On phone: fully close Expo Go (swipe away), reopen and scan QR
```
**Debug tip**: Change visible text (like screen title) to verify code is loading.
### Error 4: Search title cut off at top of screen
**Root cause**: `SafeAreaView` not providing enough inset, or content rendering behind status bar.
**Fix**: Use `SafeAreaView` from `react-native-safe-area-context` with `edges={['top']}` wrapping the content, and add sufficient `paddingTop` to the header.
### Error 5: Keyboard covers content while editing notes
**Root cause**: ScrollView doesn't auto-scroll to keep cursor visible.
**Fix**: Add `automaticallyAdjustKeyboardInsets` to ScrollView:
```jsx
<ScrollView automaticallyAdjustKeyboardInsets>
```
### Error 6: URL params not working with Expo Router
**Root cause**: Expo Router's typed routes reject dynamic query params like `?q=searchterm`.
**Fix**: Use React Context instead of URL params for passing search state between screens.
### Error 7: Claude API timeout / hanging
**Root cause**: API calls sometimes hang indefinitely.
**Fix**: AbortController with 30-second timeout on every fetch call.
### Error 8: CORS errors in web browser
**Root cause**: Claude API doesn't allow browser-origin requests.
**Fix**: Disable AI features when `Platform.OS === 'web'`. Show message directing users to use the phone app.
---
## Step 7: App Structure
```
MyApp/
├── app/
│ ├── _layout.tsx # Root layout (NotesProvider, ThemeProvider, Stack)
│ ├── (tabs)/
│ │ ├── _layout.tsx # Tab bar (Home, Search)
│ │ ├── index.tsx # Home: categories grid, recent notes, batch organize
│ │ └── explore.tsx # Search: semantic + keyword search with snippets
│ ├── note/[id].tsx # Note detail: view/edit, highlighting, delete
│ ├── new-note.tsx # Create note: single text input
│ ├── category/[id].tsx # Category detail: notes filtered by category
│ ├── organize.tsx # Smart organize: merge, recategorize, stats
│ ├── settings.tsx # API key, sync code, cloud sync
│ └── modal.tsx # Placeholder modal
├── components/
│ ├── note-card.tsx # Note preview card with category badge
│ ├── category-card.tsx # Category tile with icon + count
│ ├── themed-text.tsx # Theme-aware text
│ ├── themed-view.tsx # Theme-aware view
│ └── ui/
│ └── icon-symbol.tsx # Cross-platform icons
├── context/
│ └── notes-context.tsx # All state management, CRUD, AI orchestration
├── services/
│ ├── ai.ts # Claude API calls + all AI prompts
│ └── supabase.ts # Supabase client + sync logic
├── constants/
│ └── theme.ts # Colors and fonts
├── hooks/
│ ├── use-color-scheme.ts # Color scheme hook
│ └── use-theme-color.ts # Theme color resolver
├── app.json # Expo config
├── package.json # Dependencies
└── tsconfig.json # TypeScript config
```
## Step 8: Running on Your Phone
### Development (with Expo Go)
```bash
npx expo start
# Scan QR code with Expo Go app on your phone
# Both devices must be on same WiFi
```
### Standalone app (permanent install)
**Option A: With Xcode (Mac + iPhone via USB)**
```bash
npx expo prebuild --platform ios
open ios/MyApp.xcworkspace
# In Xcode: select your phone, set Team to your Apple ID, hit Play
```
- Free Apple ID: app works for 7 days, then reinstall
- Paid Apple Developer ($99/yr): permanent
**Option B: EAS Build (cloud, no Xcode needed)**
```bash
npm install -g eas-cli
eas login
eas build --platform ios --profile preview
# Download and install the .ipa on your phone
```
Requires paid Apple Developer account.
## Step 9: Key Lessons Learned
1. **Claude Haiku doesn't follow format instructions well.** Always build robust parsers with multiple fallback strategies. Never trust that the response will be clean JSON.
2. **Use simple IDs in AI prompts.** Timestamp IDs like `1712345678901` confuse the model. Use `[0], [1], [2]` indices and map back to real IDs in code.
3. **Pipe format > JSON for structured responses.** `0|snippet text` is more reliable than asking for JSON arrays from Haiku.
4. **Context-aware categorization needs examples.** Passing category names alone isn't enough — include 2-3 sample note titles per category so the AI understands what belongs where.
5. **Categorize by intent, not keywords.** "Medicine for trip" is Travel, not Health. The prompt must explicitly teach this with examples.
6. **Kill stale Expo processes.** Multiple Metro bundlers cause "changes not showing" confusion. Always `pkill -f expo` before restarting.
7. **Local-first architecture is essential.** Save to AsyncStorage first, sync to cloud in background. Users expect instant response.
8. **Debounce AI processing.** Don't call the API on every keystroke. Wait 10 seconds of inactivity, then process.
9. **`automaticallyAdjustKeyboardInsets`** on ScrollView is the simplest way to handle keyboard avoidance in React Native.
10. **React Context > URL params** for passing complex state (like search queries and snippet maps) between screens in Expo Router.
## Cost Estimate
- **Supabase**: Free tier (500MB database, 50K monthly active users)
- **Claude API**: ~$0.001-0.005 per note processed (Haiku is very cheap)
- Categorize: ~200 input tokens, ~20 output tokens
- Format: ~500 input tokens, ~500 output tokens
- Search: ~1000 input tokens (depends on note count), ~100 output tokens
- **Apple Developer Account**: $99/year (only needed for permanent phone install)
- **Expo**: Free for development
For personal use with ~50 notes, expect < $1/month in API costs.

Comments