top of page
Search

Notes that Think - AI powered Notes App

  • Writer: Aayushi Srivastava
    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.

 
 
 

Recent Posts

See All

Comments


bottom of page