We consume more information than ever, yet retain less. Social media feeds optimize for engagement, not understanding. Search engines return links, not knowledge. The gap between “I read about that” and “I understand that” keeps widening.
Follow The Rabbits is my attempt to bridge that gap—a mobile app that turns curiosity into structured learning. Pick any topic, and the app generates AI-curated facts backed by real web sources. Bookmark what interests you, and it creates personalized quizzes. Keep learning, and it generates entirely new content without repetition.
The name comes from “going down the rabbit hole”—that satisfying feeling when one discovery leads to another. The app is designed to facilitate that journey.
Architecture Overview
The system follows an asynchronous batch processing pattern rather than real-time generation:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Mobile App │────►│ Express │────►│ MongoDB │
│ React Native│◄────│ Backend │◄────│ (Users) │
└─────────────┘ └──────┬──────┘ └─────────────┘
│
┌──────▼──────┐ ┌─────────────┐
│ Batch │────►│Elasticsearch│
│ Processor │◄────│ (Facts) │
└──────┬──────┘ └─────────────┘
│
┌──────▼──────┐
│ Claude CLI │
│ + WebSearch │
└─────────────┘Why batch processing instead of real-time? Two reasons: quality and cost. Real-time generation would mean either waiting 1-3 minutes per request or sacrificing source verification. Batch processing lets us generate rich, source-backed content asynchronously while users browse their existing feeds.
Claude CLI as AI Backend
The most unconventional architectural decision was using the Claude CLI as a backend service. Rather than calling an API, the batch processor spawns Claude as a child process:
const claude = spawn('claude', [
'-p', '-', // Read prompt from stdin
'--output-format', 'text',
'--system-prompt', QUERY_SYSTEM_PROMPT,
'--allowedTools', 'WebSearch' // Enable web search
]);
// Write prompt via stdin to avoid shell escaping issues
claude.stdin.write(prompt);
claude.stdin.end();This approach unlocks web search capabilities—Claude can verify facts against current sources, cite recent research, and provide confidence scores based on source quality. The trade-off is latency: a single topic generation takes 1-3 minutes with web search enabled.
Key implementation details:
- 15-minute timeout for complex topics requiring extensive research
- Progress logging every 10 seconds during long operations
- Zod schema validation on output to catch malformed JSON
- Stdin for prompts to avoid shell escaping issues with complex queries
Multi-Dimensional Fact Generation
Each generated fact isn’t just a statement—it’s a learning object with five representations:
interface Fact {
id: string;
fact: string; // Core statement (1-2 sentences)
category: 'discovery' | 'method' | 'application' | 'trend' | 'debate';
inDepth: {
explanation: string; // 2-3 paragraph deep dive
context: string; // Why this matters
relatedTopics: string[];
};
eli5: string; // "Explain Like I'm 5" version
sources: Array<{
url: string;
title: string;
snippet: string;
}>;
confidence: number; // 0-1 based on source quality
followUpQuestions: Array<{
question: string;
answer: string;
}>;
}The five categories ensure variety:
| Category | Purpose | Example |
|---|---|---|
| Discovery | Recent findings | ”Researchers found that…” |
| Method | How things work | ”The process involves…” |
| Application | Practical uses | ”This is used in…” |
| Trend | Emerging patterns | ”There’s growing interest in…” |
| Debate | Open questions | ”Scientists disagree about…” |
The confidence score reflects source reliability—0.7-0.9 for emerging research, 0.9+ for well-established findings. Users can see at a glance which facts are speculative versus proven.
The Continuous Learning Engine
The hardest problem wasn’t generating facts—it was generating new facts. Users who exhaust their initial feed shouldn’t see the same content rephrased. They should discover genuinely new aspects of their topic.
The solution: context-aware regeneration. When a user marks a feed as “read,” the next batch run:
- Fetches the previous 15 facts as history
- Passes them to Claude with explicit instructions: “Cover different aspects. Don’t repeat these facts.”
- Generates content covering new angles, recent developments, deeper technical details
// In processQueryFeeds.ts
if (feed.isRead) {
const previousFacts = await getFactHistory(feed.id, 15);
prompt = buildPrompt({
query: feed.query,
level: feed.level,
previousFacts: previousFacts.map(f => f.fact),
instruction: "Generate facts covering DIFFERENT aspects than listed above."
});
}This creates infinite depth per topic. A user learning about “machine learning” might start with fundamentals, then progress to optimization techniques, then recent transformer architectures, then emerging research—all automatically, without manual curriculum design.
Smart Caching Strategy
Caching is critical when generation takes minutes. The system uses three tiers:
L1: AsyncStorage (Mobile)
- Persists topics, facts, quiz state locally
- Enables offline reading
- Selective persistence—loading states are NOT persisted
L2: MongoDB (User State)
- Bookmarked facts, quiz attempts, reading history
- Per-user data that shouldn’t be shared
L3: Elasticsearch (Generated Content)
- 7-day TTL cache for generated facts
- SHA256 hash of normalized query + difficulty level as key
- Status tracking:
pending → generating → complete → failed
Query normalization ensures cache hits across variations:
function hashQuery(query: string, level: string): string {
const normalized = query.toLowerCase().trim().replace(/\s+/g, ' ');
return crypto
.createHash('sha256')
.update(`${normalized}:${level}`)
.digest('hex')
.slice(0, 16);
}
// "Machine Learning", "machine learning", "MACHINE LEARNING"
// all hash to the same keyQuiz Generation Pipeline
Quizzes are generated from bookmarked facts, not the entire feed. This ensures users are tested on content they’ve shown interest in.
The pipeline:
- User bookmarks a fact (tap the bookmark icon)
- Fact snapshot saved to MongoDB with
hasQuiz: false - Batch processor detects unbookmarked facts
- For each fact, generate 2-3 multiple choice questions
// In quizGenerator.ts
async function generateQuiz(fact: Fact): Promise<Quiz[]> {
const context = buildFactContext(fact);
// Includes: fact statement, ELI5, explanation, follow-up Q&A
const prompt = `Generate 2-3 multiple choice questions about this fact.
Each question should have 4 options with exactly one correct answer.
Difficulty: ${fact.feedLevel}
Context: ${context}`;
return await runClaude(prompt, quizSchema);
}Difficulty scales with feed level:
- Beginner: Straightforward recall questions
- Familiar: Application and comparison questions
- Expert: Nuanced distinctions and edge cases
Technical Highlights
Provider Pattern for Extensibility
Facts come from different sources—regular feeds, bookmarked facts, potentially collaborative feeds in the future. The IFactProvider interface standardizes retrieval:
interface IFactProvider {
getFacts(userId: string, feedId: string): Promise<FactResponse>;
getStatus(userId: string, feedId: string): Promise<FactStatus>;
}
// FeedFactProvider - regular topic feeds
// SavedFactProvider - user's bookmarked facts
// Factory routes to correct provider based on feed typeZustand State Management
The mobile app uses Zustand with a slices pattern—each domain has its own slice (auth, topics, facts, quizzes, saved facts, UI state). Critical for testing and maintaining separation of concerns.
Selective persistence excludes transient state:
persist(
(set, get) => ({ ...factsSlice(set, get) }),
{
name: 'facts-storage',
partialize: (state) => ({
factsByTopic: state.factsByTopic,
// Exclude: loading, error, autocomplete
}),
}
)Lessons Learned
1. CLI tools as backend services work. It’s unconventional, but spawning Claude as a child process with web search enabled provides capabilities that aren’t available through standard APIs. The latency trade-off is acceptable for batch processing.
2. Batch processing requires excellent status UX. Users need to understand why content isn’t instant. Clear status states (pending → generating → complete), progress indicators, and the ability to see existing content while new content generates are essential.
3. Deduplication is harder than generation. Getting Claude to generate different content required careful prompt engineering—explicit history context, negative instructions (“don’t repeat”), and category constraints to ensure variety.
4. Web search dramatically improves accuracy. Without source verification, AI-generated educational content would be unreliable. The latency cost of web search is worth the quality improvement.
Follow The Rabbits is free to use at followtherabbits.com. Pick a topic you’re curious about and start learning—one rabbit hole at a time.