IMPORTANT: At the end of every work session (or after completing a significant task), you MUST:
- Update the Current Status section below to reflect what was accomplished and what comes next
- Update the Development Phases checkboxes to mark completed items
- Add any important decisions to the Decisions Log with the date
- Update
~/.claude/projects/-Users-kai-goodnotes-clone/memory/MEMORY.mdwith any new learnings, gotchas, or patterns discovered - If a new file/pattern was established, note it in memory so future sessions don't have to re-discover it
This ensures the next Claude Code session can pick up exactly where we left off.
IMPORTANT: After implementing every new feature or significant code change, you MUST run these checks before considering the work done:
pnpm run typecheck- Runs
tsc --noEmitacross all workspaces via Turbo - Catches type errors, missing imports, incorrect function signatures
- Must pass with zero errors before moving on
pnpm run test- Runs Vitest across all workspaces (shared + web) via Turbo
- After implementing a feature, write tests for it — cover:
- Utility/library functions (validation, transformations, helpers)
- API route handlers (request/response, error cases)
- Zustand stores (state transitions, actions)
- React components (rendering, user interactions, edge cases)
- Test files live next to the code they test:
foo.ts→foo.test.ts - All tests must pass before moving on
pnpm run build- Run after completing a full feature (not every small change)
- Catches Next.js-specific issues (server/client boundaries, dynamic imports)
Implement feature → Write tests → pnpm run typecheck → pnpm run test → (pnpm run build if full feature)
If any check fails, fix the issue before continuing to the next task.
- Test Runner: Vitest (v4+)
- Component Testing: @testing-library/react + @testing-library/jest-dom
- Environment: jsdom
- Shared config:
packages/shared/vitest.config.ts - Web config:
apps/web/vitest.config.ts - Web setup:
apps/web/src/test/setup.ts(loads jest-dom matchers)
An intelligent PDF annotation tool combining the smooth UX of GoodNotes with AI-powered contextual explanations. Users can upload PDFs, highlight text with drag-and-drop, and get instant AI explanations of selected content with full document context awareness.
- Seamless PDF Experience: Smooth, intuitive PDF viewing and annotation
- AI-Powered Learning: Context-aware explanations via Claude API
- Simple & Focused: Login → Upload → Annotate → Learn
- Framework: Next.js 14 (App Router)
- Language: TypeScript
- Styling: Tailwind CSS + shadcn/ui
- State Management: Zustand
- PDF Rendering: React-PDF (PDF.js wrapper)
- API: Next.js API Routes
- Authentication: Clerk
- Database: PostgreSQL (via Supabase)
- ORM: Prisma
- File Storage: Supabase Storage
- Primary AI: Anthropic Claude API (Sonnet 4.5)
- PDF Text Extraction: pdfjs-dist
- Context Management: Custom chunking strategy
- id (UUID)
- clerkId (string, unique)
- email (string)
- name (string)
- createdAt, updatedAt (DateTime)
- id (UUID)
- userId (UUID, foreign key)
- title, fileName, fileUrl (string)
- fileSize, pageCount (number)
- uploadedAt, lastOpenedAt (DateTime)
- id (UUID)
- documentId, userId (UUID, foreign keys)
- type (enum: 'highlight', 'note', 'ai_explanation')
- pageNumber (number)
- color (string)
- position (JSON) // {x, y, width, height}
- selectedText, content (text)
- createdAt, updatedAt (DateTime)
- id (UUID)
- documentId (UUID, foreign key)
- extractedText (text) // Full document text
- chunkMetadata (JSON)
- createdAt (DateTime)
- Flow: Landing page → Sign in with Clerk → Dashboard
- Pages:
/,/sign-in,/sign-up,/dashboard
- Flow: Dashboard → Upload button → File picker → Processing → Document list
- Validation: PDF only, max 50MB
- Processing: Extract text, count pages, save to storage
- Flow: Click document → Full-screen viewer
- Features: Page navigation, zoom controls, thumbnails
- Flow: Click highlight button → Drag across text → Text highlighted
- Features: Color picker, multi-select, edit/delete, persist
- Flow: Toggle AI mode → Select text → Sidebar shows AI explanation
- Features: Streaming responses, context-aware, conversation history
- Clean & minimal white/light gray background
- PDF takes center stage
- Smooth animations (200-300ms transitions)
- Color palette: Blue primary, translucent highlights
- Clear hover/active states
- Skeleton loading screens
- Inline error messages
- Encouraging empty states
goodnotes-clone/
├── apps/web/ # Next.js web app
│ ├── src/
│ │ ├── app/
│ │ │ ├── (auth)/sign-in, sign-up
│ │ │ ├── (dashboard)/dashboard, document/[id]
│ │ │ └── api/ (documents, annotations, ai, folders, conversations)
│ │ ├── components/ (ui/, pdf/, ai/, folders/, history/, layout/)
│ │ ├── lib/ (db, auth, pdf, ai, ai-client, annotations, screenshot, storage, sync-*, supabase-client)
│ │ ├── hooks/ (use-ai-chat, use-keyboard-shortcuts, use-region-select, use-text-selection, use-toast, use-sync, use-drawing-save)
│ │ ├── stores/ (folder-store, conversation-store)
│ │ └── middleware.ts
│ ├── prisma/ (schema.prisma, migrations/)
│ ├── public/ (pdf.worker.min.mjs)
│ ├── next.config.mjs, tailwind.config.ts, vitest.config.ts
│ └── package.json (@cookednote/web)
├── packages/shared/ # Cross-platform business logic
│ └── src/
│ ├── types/index.ts # All TypeScript interfaces and constants
│ ├── stores/ # pdf-store, annotation-store, ai-store, drawing-store, sync-store (+tests)
│ └── lib/ # utils (cn), ai-prompts, stroke-renderer, sync-adapter
├── apps/mobile/ # Expo mobile app
│ ├── app/
│ │ ├── _layout.tsx # Root Stack layout with Clerk auth
│ │ ├── (auth)/ # Sign-in, sign-up screens
│ │ ├── (tabs)/ # Library, Recent, Settings tabs
│ │ └── document/[id].tsx # Full-screen PDF viewer
│ ├── components/ # PDFViewer, PDFToolbar, ThumbnailSidebar, DocumentGrid, DocumentCard, EmptyState, SyncIndicator, DrawingCanvas, DrawingToolbar, AIBottomSheet, MessageBubble, RegionSelectOverlay, ScreenshotCarousel, ConversationHistory
│ ├── hooks/ # use-documents, use-drawing-save, use-sync, use-ai-chat
│ ├── stores/ # conversation-store
│ ├── lib/ # api, pdf-cache, constants, sync-*, background-sync, ai-client, screenshot, share-utils
│ ├── app.json, metro.config.js, babel.config.js
│ └── package.json (@cookednote/mobile)
├── turbo.json
├── pnpm-workspace.yaml
├── tsconfig.json # Base TS config
├── package.json # Root workspace (cookednote)
├── claude.md
└── commands/, docs/
- Initialize project structure
- Set up authentication flow
- Create basic layouts and routing
- Configure database
- Implement PDF upload with validation
- Create document list view
- Build PDF viewer component
- Add page navigation and zoom
- Build highlight tool UI
- Implement text selection detection
- Create annotation storage
- Add color picker and editing
- Set up Claude API integration
- Implement PDF text extraction
- Build AI sidebar component
- Create context management
- Add streaming responses
- Use Playwright MCP for UX review
- Implement loading states
- Add error handling
- Optimize performance
- Deploy to Vercel
- Folder management system (create, rename, delete, nested, color-coded, drag-and-drop)
- LaTeX math rendering in AI responses (KaTeX + react-markdown + remark-math)
- Conversation persistence & history (save, list, reopen, badges on PDF pages)
- Conversation history page (/dashboard/conversations)
- Mobile responsive fixes for folder sidebar
- Switch from npm to pnpm
- Create turbo.json and pnpm-workspace.yaml
- Move Next.js app to apps/web/
- Extract shared code to packages/shared/ (types, stores, utils, ai-prompts)
- Update all imports to use workspace packages
- Verify web app works (229 tests, typecheck, build all pass)
- Create Expo app in apps/mobile/ (manual setup for monorepo)
- Install core deps (expo-router, expo-secure-store, lucide-react-native)
- Set up Expo Router file-based routing
- Implement tab navigation (Library, Recent, Settings)
- Add @clerk/clerk-expo authentication flow
- Wire up shared stores from @cookednote/shared
- Test on iOS Simulator + real iPad
- Implement react-native-pdf wrapper component
- Add virtualized page rendering (lazy load + buffer)
- Implement pinch-to-zoom with momentum gestures
- Add thumbnail sidebar navigation
- Local PDF caching with expo-file-system
- Test with large PDFs (100+ pages, 50MB+)
- Verify 60fps scrolling performance
- Implement @shopify/react-native-skia canvas overlay on PDF
- Add gesture detection (pan, force/pressure) via react-native-gesture-handler
- Implement stroke smoothing (perfect-freehand library)
- Apple Pencil pressure sensitivity (event.force)
- Palm rejection heuristics (multi-pointer rejection)
- Pen/highlighter/eraser tool modes
- Undo/redo with action stack
- Save strokes via API (drawing annotation type in existing Annotation model)
- Add web DrawingLayer (HTML Canvas + Pointer Events) for parity
- Optimize to <16ms latency, test on real iPad with Apple Pencil
- Implement SyncQueue in packages/shared/ (offline-first)
- Add background sync worker (expo-task-manager)
- Supabase Realtime subscriptions for cross-device sync
- Conflict resolution (last-write-wins with syncVersion)
- Test offline → online recovery
- Sync status indicators in UI
- Add syncVersion + updatedAt to Prisma schema + batch sync API
- Port AI sidebar to bottom sheet (@gorhom/bottom-sheet v5)
- Screenshot capture with react-native-view-shot + expo-image-manipulator
- Reuse @cookednote/shared AI prompts + streaming SSE (mobile ai-client)
- Keyboard-aware scrolling, swipe to dismiss (bottom sheet snap points)
- Screenshot preview carousel (ScreenshotCarousel component)
- Copy/share AI responses (expo-clipboard + RN Share)
- Conversation persistence (mobile conversation store with apiFetch)
- Region selection overlay (gesture handler + reanimated)
- Conversation history (load, delete saved conversations)
- Add Subscription + UsageRecord models to Prisma schema
- Subscription types, constants, utility functions in shared types
- Shared Zustand subscription-store with adapter injection pattern
- Server-side quota library (check/increment document + AI usage)
- Subscription status API (GET /api/subscription)
- Quota enforcement on upload + AI routes (402 on exceeded)
- Stripe integration (checkout, portal, webhook handler)
- RevenueCat integration (webhook handler, SDK wrapper)
- Web subscription UI (paywall dialog, upgrade banner, settings page, badge)
- Mobile subscription UI (paywall screen, settings subscription section)
- Security hardening (timing-safe auth, TOCTOU fix, UUID validation)
- Test purchase flow in sandbox (Stripe test mode + RevenueCat sandbox)
- Set up App Store Connect + Google Play Console
- Run full testing checklist (iPad Pencil, iPhone, Android)
- Fix critical bugs (crash, data loss, sync, purchase)
- Performance targets met (<16ms draw, <2s launch, <1s PDF open)
- App Store screenshots/preview videos
- Privacy policy + support docs
- Beta test with TestFlight (10-20 users)
- Address beta feedback
- Submit to App Store for review
- Release v1.0
- ProductHunt, social media, iPad productivity outreach
- Monitor crash reports + reviews
- Rapid bug fix cycle (1-2 day turnaround)
- Port to Android (reuse ~80% codebase)
- Samsung S-Pen + stylus pressure support
- Material Design 3 theming
- Google Play submission
- Update web app with learnings from mobile
- Cross-platform feature parity
Problem: PDF.js renders text in canvas/SVG, making selection tricky Solution: Use PDF.js text layer API with custom selection logic
Problem: Storing positions that work across zoom levels Solution: Store normalized coordinates (0-1 range) relative to page
Problem: Large PDFs exceed Claude's context window Solution: Extract full text on upload, send relevant pages + context for queries
Problem: Keep annotations smooth and responsive Solution: Optimistic UI updates, debounced saves, Zustand for local state
Problem: Large PDFs slow to render Solution: Lazy load pages, cache rendered pages, use Web Workers
POST /api/documents/upload- Upload new PDFGET /api/documents- List user's documentsGET /api/documents/[id]- Get document detailsDELETE /api/documents/[id]- Delete document
POST /api/annotations- Create annotationGET /api/annotations?documentId=[id]- Get annotationsPATCH /api/annotations/[id]- Update annotationDELETE /api/annotations/[id]- Delete annotation
POST /api/ai/chat- AI conversation with vision support
POST /api/sync- Batch sync actions (up to 50)GET /api/sync?since=<timestamp>- Catch-up sync (entities modified since timestamp)
GET /api/subscription- Get subscription + usage statusPOST /api/stripe/checkout- Create Stripe Checkout SessionPOST /api/stripe/portal- Create Stripe Billing Portal SessionPOST /api/webhooks/stripe- Stripe webhook handlerPOST /api/webhooks/revenuecat- RevenueCat webhook handler
- User can sign up and log in
- User can upload a PDF
- User can view PDF with smooth navigation
- User can highlight text in multiple colors
- User can select text and get AI explanation
- Highlights persist across sessions
- AI responses are contextually relevant
- Page load: < 2s
- PDF render: < 3s for 10-page doc
- Highlight response: < 100ms
- AI response start: < 1s (streaming)
- Invoke the ui-ux-reviewer subagent to review your work and implement suggestions where needed
- Iterate on the review process when needed
Phase: Phase 14 in progress (Polish & Launch Prep) — real-device testing underway
What's done:
- Phases 1-6: Full web MVP (auth, upload, PDF viewer, highlights, AI, folders, conversations, LaTeX)
- Phase 7: Turborepo monorepo with
apps/web/+packages/shared/ - Phase 8: Expo mobile app scaffold at
apps/mobile/with Clerk auth, tab navigation, shared types - Phase 9: PDF viewer mobile (react-native-pdf, caching, document grid, thumbnails)
- Phase 10: Handwriting Engine (cross-platform drawing)
- Phase 11: Sync & Cloud (offline-first sync queue, Supabase Realtime, batch sync API)
- Phase 12: AI Integration Mobile (bottom sheet, region select, screenshots, streaming, conversations)
- Phase 13: Monetization:
- Prisma Subscription + UsageRecord models
- Shared subscription-store (Zustand) with platform-specific fetch adapters
- Server-side quota enforcement on upload (402 + TOCTOU-safe increment/rollback) and AI routes
- Stripe integration: checkout sessions, billing portal, webhook handler (v20 compatible)
- RevenueCat integration: webhook handler (timing-safe auth, UUID validation), SDK wrapper
- Web UI: paywall dialog, upgrade banner, subscription badge, settings page
- Mobile UI: paywall screen (RevenueCat purchase/restore), settings subscription section
- Security: timing-safe webhook auth, TOCTOU race prevention, UUID validation, env var docs
- Phase 14 (in progress): Real iPad device testing bug fixes:
- Fixed library page infinite re-render jitter (useApiFetch useRef pattern)
- Added mobile PDF upload (expo-document-picker + FAB button on library)
- Fixed DrawingCanvas not rendering in document screen (was never mounted)
- Fixed Skia v2.4.21 → v1.5.0 downgrade for Expo SDK 52 compatibility
- Fixed reanimated worklet errors in DrawingCanvas (runOnJS for gesture callbacks)
- Fixed gesture conflicts (removed nested GestureHandlerRootView, pointerEvents on PDF)
- Added graceful Skia fallback with warning banner for Expo Go
- Fixed drawing latency — bypassed Zustand store during active drawing, accumulate raw points in useRef, render simple moveTo/lineTo line segments, apply perfect-freehand only on stroke end, throttle re-renders to 60fps via requestAnimationFrame
- Fixed worklet ref snapshot bug — canvasSizeRef was frozen in worklet closure at {1,1}, moved coordinate normalization from worklet to JS thread
- Fixed Apple Pencil pressure — clamped force (0–6.67) to 0–1 for Zod validation
- Fixed stroke commit — replaced fragile beginStroke/endStroke dance with direct pageStrokes commit using refs captured at stroke begin time
- Improved save error logging — PATCH/POST errors now include HTTP status + response body
- App: CookedNote | Bundle ID:
com.cookednote.app| SDK: Expo 52 - 344 tests passing (199 shared + 145 web) | All 3 workspaces typecheck clean
What's next:
- Continue Phase 14: Verify drawing latency improvement on real iPad, test stroke persistence across sessions
- Mobile highlight tool not yet implemented (no HighlightLayer on mobile)
- Still need
prisma db pushwhen Supabase is reachable (to apply Subscription/UsageRecord + syncVersion columns) - Enable Supabase Realtime on Annotation, Document, Folder tables in Supabase dashboard
- Test Stripe checkout + RevenueCat sandbox purchase flows
- Test with large PDFs (100+ pages, 50MB+)
Open questions:
- Target launch date?
- Budget for services? (Supabase tier, Anthropic API limits)
- Beta tester access? (need real iPad users)
- Design assets ready? (app icon, screenshots)
- Legal review completed? (privacy policy, ToS)
- 2026-02-05: Chose Next.js App Router for better server components
- 2026-02-05: Chose Clerk over NextAuth for faster auth setup
- 2026-02-05: Using Supabase for database + storage (all-in-one)
- 2026-02-05: Desktop-first approach, mobile later
- 2026-02-05: Using React-PDF over PSPDFKit (cost-effective)
- 2026-02-05: Prisma 7 uses prisma-client (not prisma-client-js) with prisma.config.ts
- 2026-02-05: Supabase requires DIRECT_URL (port 5432) for Prisma CLI; pooled URL (port 6543) for runtime
- 2026-02-05: Clerk v6 requires
clerkMiddleware(not deprecatedauthMiddleware),auth()must be awaited, imports from@clerk/nextjs/server - 2026-02-05: Prisma 7 requires
@prisma/adapter-pgdriver adapter (no built-in query engine), import from@/generated/prisma/client - 2026-02-05: Clerk sign-in/sign-up pages use
[[...sign-in]]catch-all routes for multi-step auth flows - 2026-02-06: PDF.js worker served from public/ (copied from node_modules) — webpack can't bundle .mjs worker files
- 2026-02-06: Server-side PDF processing uses
pdfjs-dist/legacy/build/pdf.mjs(avoids DOMMatrix dependency) - 2026-02-06: Supabase Storage uses service role key server-side, bucket "documents", path
{userId}/{timestamp}-{fileName} - 2026-02-06: @testing-library/react needs explicit cleanup() in vitest setup when globals: true not set
- 2026-02-06: Annotation positions stored as normalized rects (0-1 range) in
{ rects: [...] }format for multi-line support - 2026-02-06: Zustand selector with
.filter()causes infinite re-renders — get full array from store, filter in component withuseMemo - 2026-02-06: react-pdf
<Page>accepts children rendered inside itsposition: relativecontainer — used for HighlightLayer overlay - 2026-02-06:
mixBlendMode: "multiply"gives natural highlighter pen appearance on PDF text - 2026-02-07: AI streaming uses Anthropic SDK
client.messages.stream()with SSE format (data: {"text":"delta"}\n\n) - 2026-02-07: Smart context chunking sends target page + adjacent pages within 8000 char budget to Claude
- 2026-02-07: Cross-store coordination (AI ↔ highlight mode) uses callback pattern to avoid circular imports between Zustand stores
- 2026-02-07:
scrollIntoView?.()with optional chaining needed for jsdom compatibility in tests - 2026-02-07: AI sidebar uses
next/dynamicwithssr: false(same pattern as PDFCanvas) for client-only rendering - 2026-02-07: Replaced text-selection AI with screenshot-based AI — draw box on PDF → capture region → send to Claude vision API
- 2026-02-07: Deleted
/api/ai/explainroute — consolidated all AI into/api/ai/chatwith vision support - 2026-02-07: Screenshot capture uses canvas
.width/.height(pixel dims, not CSS) for correct DPR handling - 2026-02-07: Base64 screenshots downscaled to max 1200px longest side to keep API payloads reasonable
- 2026-02-07: Region selection hook uses mousedown/move/up with normalized 0-1 coords, min 10px threshold
- 2026-02-07:
<img>used for base64 screenshots instead ofnext/image(data URLs can't use next/image optimization) - 2026-02-07:
@testing-library/reactusesgetByAltText(notgetByAlt) for querying by alt attribute - 2026-02-07: Continuous scroll uses IntersectionObserver for page tracking + virtualization buffer of +/-2 pages
- 2026-02-07:
usePDFStore.setState({ currentPage })used directly (notsetCurrentPage) from observer to avoid setting scrollTarget - 2026-02-07: IntersectionObserver not in jsdom — mock with class (not vi.fn arrow) since it needs
newconstructor - 2026-02-07:
NodeListOf<Element>can't be iterated withfor...ofin TS default target — useArray.from()first - 2026-02-08: Folder model uses self-referential relation
@relation("FolderNesting")for parent/children nesting - 2026-02-08: Document.folderId uses
onDelete: SetNull— deleting folder doesn't delete documents - 2026-02-08: Folder deletion reparents children to parent folder (or root) and moves documents to root
- 2026-02-08: Conversation screenshots stored as JSON string in DB, parsed on client
- 2026-02-08: LaTeX rendering uses
react-markdown+remark-math+rehype-katexwithkatex/dist/katex.min.cssimport - 2026-02-08: Conversation reopen from history uses query params
?conversation={id}&page={num}on document URL - 2026-02-08: Folder sidebar hidden on mobile (
hidden md:block) — mobile nav uses shorter labels - 2026-02-08:
prisma db pushused instead ofprisma migrate devdue to Supabase shadow database limitations - 2026-02-09: Monorepo migration — Turborepo with pnpm workspaces (
apps/web/+packages/shared/) - 2026-02-09: Shared package uses subpath exports (
@cookednote/shared/types,@cookednote/shared/stores/*, etc.) - 2026-02-09: No build step for shared — Next.js
transpilePackages: ["@cookednote/shared"]consumes raw TS - 2026-02-09: ai.ts split — pure prompt builders in shared,
getAnthropicClient()stays in web with re-exports - 2026-02-09: Prisma stays in
apps/web/(server-only, can extract to packages/db later) - 2026-02-09: pnpm requires
onlyBuiltDependenciesin root package.json for native deps (prisma, esbuild, clerk) - 2026-02-09: Turbo requires
packageManagerfield in root package.json - 2026-02-10: Mobile app uses
@clerk/clerk-expov2 withtokenCachefrom@clerk/clerk-expo/token-cache(expo-secure-store) - 2026-02-10: Manual Expo project setup (not create-expo-app) for clean monorepo integration
- 2026-02-10: Metro config:
unstable_enablePackageExports: truefor shared subpath exports,watchFoldersfor live reload - 2026-02-10: StyleSheet (not NativeWind) for initial scaffold — avoids NativeWind v5 complexity
- 2026-02-10:
lucide-react-nativefor tab icons (matches web'slucide-react) - 2026-02-13:
react-native-pdfv6.7.6 +@config-plugins/react-native-pdfv9.0.0 for PDF rendering (native PDFKit/PdfRenderer) - 2026-02-13:
react-native-blob-utilrequired as peer dep for react-native-pdf file access - 2026-02-13:
@shopify/flash-listv1.7.3 for document grid (recycled rendering, SDK 52 compatible) - 2026-02-13: PDF caching:
expo-file-systemdownloads tocacheDirectory/pdfs/,AsyncStoragemaps docId → path - 2026-02-13: API auth:
useApiFetch()hook wraps fetch with Clerk JWT; mobile + web share same Clerk app - 2026-02-13:
useApiFetchmust useuseCallbackto stabilize function identity — otherwise causes infinite re-render inuseDocuments - 2026-02-13: Root layout changed from
<Slot>to<Stack>to supportdocument/[id]route outside tabs - 2026-02-13:
react-native-reanimatedplugin must be last in babel.config.js plugins array - 2026-02-13: Drawing annotations use existing Annotation model with
type: "drawing"andposition: { strokes: [...] }(DrawingPosition) - 2026-02-13:
perfect-freehandinstalled inpackages/shared/— cross-platform stroke smoothing with pressure sensitivity - 2026-02-13: AnnotationPosition = HighlightPosition | DrawingPosition (discriminated union with type guards)
- 2026-02-13: Drawing store uses Map<number, DrawingStroke[]> for per-page strokes with Set dirty page tracking
- 2026-02-13: Cross-store coordination expanded: drawing/highlight/AI modes are mutually exclusive (callbacks + direct imports)
- 2026-02-13: Web DrawingLayer uses Canvas2D + offscreen canvas for committed strokes + rAF for active stroke
- 2026-02-13: Mobile DrawingCanvas uses @shopify/react-native-skia + Gesture.Pan() for GPU-accelerated drawing
- 2026-02-13: Zustand Map selector creates new
[]ref each render — useuseMemoto stabilize (same pattern as annotation filter) - 2026-02-14: AI bottom sheet uses @gorhom/bottom-sheet v5 with snap points ["25%", "60%", "90%"] for collapsed/reading/typing states
- 2026-02-14: Mobile region selection: Gesture.Pan + reanimated shared values for 60fps selection rectangle, min 20px threshold
- 2026-02-14: Screenshot capture: react-native-view-shot captures PDF view, expo-image-manipulator crops to region + downscales
- 2026-02-14: Mobile SSE streaming reuses web's parseSSEStream pattern (ReadableStream + TextDecoder) with apiFetch for JWT auth
- 2026-02-14: Mobile conversation store uses token provider pattern:
setConversationTokenProvider(getToken)initialized in _layout.tsx - 2026-02-14: react-native-markdown-display for AI responses; LaTeX skipped for v1 (renders as inline code)
- 2026-02-14: GestureHandlerRootView wraps document screen for bottom sheet + region selection gesture coordination
- 2026-02-15: Stripe SDK v20 breaking changes —
current_period_start/endmoved to SubscriptionItem,Invoice.subscriptionmoved toInvoice.parent.subscription_details - 2026-02-15: Monetization uses dual provider architecture: Stripe (web) + RevenueCat (mobile) write to same Subscription table with
providerdiscriminator - 2026-02-15: Quota enforcement uses TOCTOU-safe pattern: increment BEFORE upload, rollback on failure via
decrementDocumentUsage - 2026-02-15: RevenueCat webhook auth uses
crypto.timingSafeEqual+ UUID regex validation onapp_user_id - 2026-02-15: Subscription store uses adapter injection pattern (same as sync-store) for platform-specific fetch
- 2026-02-15: Free tier: 3 docs + 10 AI requests/month; Pro: $7.99/mo or $49.99/yr, unlimited
- 2026-02-16:
useApiFetchinfinite re-render fix —useAuth().getTokenis unstable (new ref each render). Store inuseRefsouseCallbackhas stable identity with[]deps - 2026-02-16: Mobile PDF upload uses
expo-document-picker+ FormData with{ uri, type, name }object (cast asunknown as Blob). Don't set Content-Type header — let RN set multipart boundary - 2026-02-16:
@shopify/react-native-skiav2.x requires React 19 + RN 0.78; Expo SDK 52 (React 18.3 + RN 0.76.9) needs v1.5.0. Usenpx expo installto auto-select compatible version - 2026-02-16: DrawingCanvas lazy-loaded with
try { require() } catchat module level for Expo Go graceful fallback (Skia native module unavailable without dev build) - 2026-02-16: RNGH v2+ gesture callbacks (
.onBegin,.onUpdate,.onEnd) run on reanimated UI thread (worklets). Call JS-thread functions viarunOnJS()— ZustandgetState(), custom helpers, etc. - 2026-02-16: Nested
GestureHandlerRootViewcauses gesture conflicts — only one GHRV per screen. DrawingCanvas uses plainViewsince document screen already has GHRV - 2026-02-16:
pointerEvents="none"on PDF wrapper when drawing mode active — preventsreact-native-pdfUIScrollView from stealing touch events from overlaid DrawingCanvas