RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/AI Models
AI Models/2026-05-05Intermediate

Build an AI-Powered Certification Exam App with Rork: Adaptive Learning That Targets Your Weak Spots

Build a certification exam prep app with Rork and Gemini API. Learn to implement adaptive quiz logic, AI-driven weakness analysis, and Supabase-backed progress tracking — from first prompt to App Store.

Rork515Gemini7AI30Exam PrepAdaptive LearningSupabase33App Development33

The most inefficient way to study for a certification exam is to review everything at the same rate — spending equal time on topics you've mastered and topics that are quietly undermining your score. Most study apps don't solve this problem. They present questions in a fixed order and leave it to you to figure out where you're weak.

This tutorial builds something better: a Rork app that tracks every answer, uses Gemini to identify your weak categories, and automatically prioritizes those topics the next time you open the app. By the end, you'll have a working adaptive learning app you can publish on the App Store.

Here's what we're building:

  • Category-based multiple-choice quizzes
  • Automatic weakness detection via Gemini API
  • Priority question selection based on your performance data
  • Visual progress analytics
  • Supabase for persistent answer history

This assumes you've shipped at least one Rork app before. If you're brand new to Rork, start with the beginner guide and come back — the Supabase and Gemini integrations here will make more sense with that foundation.

Designing the Data Model First

Before writing a single prompt in Rork, it's worth spending five minutes on the data model. Adaptive learning lives or dies on the quality of data you collect, and fixing a bad schema mid-build is painful.

Here's the structure we'll use:

users           — user accounts (Supabase Auth)
questions       — question bank (category, options, correct answer, explanation)
answer_logs     — every answer with timestamp and time spent
weak_analysis   — AI-generated weakness reports (refreshed every 10 questions)

Create these tables in your Supabase project with this SQL:

CREATE TABLE questions (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  category TEXT NOT NULL,
  difficulty INTEGER DEFAULT 1,        -- 1: easy, 2: medium, 3: hard
  question_text TEXT NOT NULL,
  options JSONB NOT NULL,              -- ["Option A", "Option B", "Option C", "Option D"]
  correct_index INTEGER NOT NULL,      -- 0–3
  explanation TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);
 
CREATE TABLE answer_logs (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID NOT NULL,
  question_id UUID REFERENCES questions(id),
  selected_index INTEGER NOT NULL,
  is_correct BOOLEAN NOT NULL,
  time_spent_seconds INTEGER,
  answered_at TIMESTAMPTZ DEFAULT NOW()
);
 
CREATE TABLE weak_analysis (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID NOT NULL,
  analyzed_at TIMESTAMPTZ DEFAULT NOW(),
  weak_categories JSONB,              -- [{"category": "Networking", "accuracy": 0.4}]
  ai_suggestion TEXT,                 -- Gemini-generated study advice
  next_question_ids UUID[]            -- IDs to prioritize next session
);

Enable Row Level Security so users only see their own data:

ALTER TABLE answer_logs ENABLE ROW LEVEL SECURITY;
 
CREATE POLICY "Users access own logs"
  ON answer_logs FOR ALL
  USING (user_id = auth.uid());

Setting Up the Rork Project

With the database ready, start a new Rork project. Give it this initial prompt to establish the app structure:

Build a React Native certification exam prep app.

Screens needed:
- Home: today's study goal, weakness summary card, start quiz button
- Quiz: one multiple-choice question at a time, answer feedback, explanation modal
- Analysis: category accuracy bar chart, AI advice card, "Focus on weak areas" button
- Settings: target exam date, daily question goal

Use Supabase for data and Gemini API for AI analysis.
Clean white UI with a sense of progress and accomplishment.

Rork will generate the scaffolding. Don't try to get everything right in one pass — getting the structure right first, then refining screen by screen, produces cleaner results than cramming all logic into the first prompt.

Smart Question Selection

The default code Rork generates for question selection is usually a naive SELECT * LIMIT 1. This works but ignores everything we know about the user's performance. Replace it with this:

// src/lib/questionSelector.ts
import { supabase } from './supabase';
 
export async function getNextQuestion(userId: string) {
  // Check whether we have a fresh weakness analysis
  const { data: analysis } = await supabase
    .from('weak_analysis')
    .select('next_question_ids, analyzed_at')
    .eq('user_id', userId)
    .order('analyzed_at', { ascending: false })
    .limit(1)
    .single();
 
  const analysisAge = analysis?.analyzed_at
    ? Date.now() - new Date(analysis.analyzed_at).getTime()
    : Infinity;
 
  const recentlyAnswered = await getRecentlyAnswered(userId);
 
  // Use priority list if analysis is fresh (< 24 hours) and has items
  if (analysisAge < 86_400_000 && analysis?.next_question_ids?.length > 0) {
    const { data: priorityQ } = await supabase
      .from('questions')
      .select('*')
      .in('id', analysis.next_question_ids)
      .not('id', 'in', recentlyAnswered.length > 0 ? `(${recentlyAnswered.join(',')})` : '(null)')
      .order('difficulty', { ascending: true })
      .limit(1)
      .single();
 
    if (priorityQ) return priorityQ;
  }
 
  // Fall back to a random question from the full bank
  const { data: pool } = await supabase
    .from('questions')
    .select('*')
    .not('id', 'in', recentlyAnswered.length > 0 ? `(${recentlyAnswered.join(',')})` : '(null)')
    .limit(20);
 
  if (!pool || pool.length === 0) return null;
  return pool[Math.floor(Math.random() * pool.length)];
}
 
async function getRecentlyAnswered(userId: string): Promise<string[]> {
  // Skip questions answered in the last 24 hours to avoid repetition
  const cutoff = new Date(Date.now() - 86_400_000).toISOString();
  const { data } = await supabase
    .from('answer_logs')
    .select('question_id')
    .eq('user_id', userId)
    .gte('answered_at', cutoff);
 
  return data?.map(d => d.question_id) ?? [];
}

The 24-hour recency filter is the part Rork won't generate on its own, and it matters more than you'd expect. Without it, the weakness analysis sends users into a loop of the same 20 questions, which kills engagement after the first session.

Connecting Gemini for Weakness Analysis

This is the feature that makes the app genuinely useful. Every 10 answered questions, we send a performance summary to Gemini and ask it to identify focus areas.

// src/lib/weaknessAnalyzer.ts
 
const GEMINI_KEY = process.env.EXPO_PUBLIC_GEMINI_API_KEY; // YOUR_GEMINI_API_KEY
 
interface AnswerLog {
  question: { category: string };
  is_correct: boolean;
  time_spent_seconds: number;
}
 
export async function analyzeAndSave(
  userId: string,
  logs: AnswerLog[]
): Promise<void> {
  // Aggregate stats per category
  const stats: Record<string, { correct: number; total: number; totalTime: number }> = {};
 
  for (const log of logs) {
    const cat = log.question.category;
    if (!stats[cat]) stats[cat] = { correct: 0, total: 0, totalTime: 0 };
    stats[cat].total++;
    if (log.is_correct) stats[cat].correct++;
    stats[cat].totalTime += log.time_spent_seconds;
  }
 
  const statsText = Object.entries(stats)
    .map(([cat, s]) => {
      const pct = ((s.correct / s.total) * 100).toFixed(0);
      const avg = (s.totalTime / s.total).toFixed(0);
      return `${cat}: ${pct}% accuracy, avg ${avg}s per question`;
    })
    .join('\n');
 
  // Ask Gemini for structured analysis
  const res = await fetch(
    `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        contents: [{ parts: [{ text: `
Analyze this exam prep performance data and return structured JSON.
 
Performance by category:
${statsText}
 
Return exactly this JSON structure:
{
  "weak_categories": [
    {"category": "name", "accuracy": 0.0–1.0}
  ],
  "focus_categories": ["top 1–3 categories to prioritize"],
  "advice": "One actionable study tip in 20 words or fewer"
}
` }] }],
        generationConfig: {
          responseMimeType: 'application/json',
          temperature: 0.2
        }
      })
    }
  );
 
  if (!res.ok) throw new Error(`Gemini error: ${res.status}`);
 
  const json = await res.json();
  const analysis = JSON.parse(json.candidates[0].content.parts[0].text);
 
  // Fetch question IDs for the focus categories
  const { data: priorityQs } = await supabase
    .from('questions')
    .select('id')
    .in('category', analysis.focus_categories)
    .order('difficulty', { ascending: true })
    .limit(30);
 
  // Persist the analysis
  await supabase.from('weak_analysis').insert({
    user_id: userId,
    weak_categories: analysis.weak_categories,
    ai_suggestion: analysis.advice,
    next_question_ids: priorityQs?.map(q => q.id) ?? []
  });
}

Two things worth noting here. First, responseMimeType: 'application/json' tells Gemini to return raw JSON — without it, you sometimes get JSON wrapped in Markdown code fences, which breaks JSON.parse. Second, temperature: 0.2 keeps the analysis consistent. Higher temperatures produce more creative but less reliable structured output.

Building the Analytics Screen

Tell Rork what you want the analysis screen to show:

Build AnalysisScreen.tsx with these elements:

1. Stats header: total questions answered, overall accuracy percentage
2. Category bar chart: one bar per category, color-coded
   - Under 50%: red (#EF4444)
   - 50–75%: amber (#F59E0B)
   - Over 75%: green (#22C55E)
3. AI advice card: white card with lightbulb icon, Gemini's advice text
4. "Practice weak areas now" CTA button
5. 7-day streak calendar at the bottom

Data comes from Supabase answer_logs and weak_analysis tables.

The color-coded bars rarely come out right on the first pass — Rork tends to use a single color. Add this as a follow-up prompt:

Update the category bars to animate from gray to their target color
when the screen loads. Use a 300ms transition.
The color should be computed from the accuracy value, not hardcoded.

Monetization: What to Gate

For a study app, the freemium split I've found most effective is:

Free tier: 10 questions per day, basic correct/wrong feedback, category accuracy totals.

Paid tier (around $4/month): unlimited questions, AI weakness analysis, difficulty auto-adjustment, push notification reminders.

Gating the AI analysis behind a paywall isn't just a business decision — it's cost management. Gemini API calls add up when your user base grows, and free users burning API budget on every 10-question interval doesn't scale. Limiting AI analysis to paying users keeps your margin positive from day one.

Wire up the paywall check before calling analyzeAndSave:

// Before triggering analysis
const { data: subscription } = await supabase
  .from('subscriptions')
  .select('status')
  .eq('user_id', userId)
  .single();
 
if (subscription?.status === 'active') {
  await analyzeAndSave(userId, recentLogs);
} else {
  // Show a gentle upsell — don't hard block, just tease
  showUpgradePrompt('Unlock AI analysis to see exactly where you need to improve');
}

Generating Your Question Bank

A functional app without questions isn't publishable. Use this script to batch-generate questions with Gemini before launch:

// scripts/seedQuestions.ts — run locally, not in the app
 
const CATEGORIES = ['Networking', 'Security', 'Databases', 'Algorithms'];
 
for (const category of CATEGORIES) {
  const res = await fetch(
    `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key=${GEMINI_KEY}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        contents: [{ parts: [{ text: `
Generate 10 multiple-choice questions for a professional certification exam.
Category: ${category}
Mix of difficulty levels 1–3.
 
Return JSON array:
[{
  "category": "${category}",
  "difficulty": 1–3,
  "question_text": "...",
  "options": ["A", "B", "C", "D"],
  "correct_index": 0–3,
  "explanation": "Brief explanation under 80 words"
}]
` }] }],
        generationConfig: { responseMimeType: 'application/json' }
      })
    }
  );
 
  const data = await res.json();
  const questions = JSON.parse(data.candidates[0].content.parts[0].text);
 
  await supabase.from('questions').insert(questions);
  console.log(`Inserted ${questions.length} ${category} questions`);
}

Before importing, manually review every question. AI-generated exam content occasionally has ambiguous wording, incorrect answers, or explanations that contradict the question. Catching these before users see them saves you bad reviews.

What to Build Next

The app you have now is functional and publishable. To take it further, the highest-impact next feature is spaced repetition scheduling — using the SM-2 algorithm to calculate optimal review intervals for each question based on how many times you've gotten it right and wrong. It turns the current "recent answers" filter into something scientifically grounded.

For now, ship what you have. Even a simple adaptive quiz with AI analysis is meaningfully better than a static flashcard app, and getting real user feedback will tell you which direction to push next.

You can read more about Rork + Gemini integration patterns and setting up Supabase authentication in Rork apps to build on what you've started here.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

AI Models2026-07-05
When Your Finance App's AI Keeps Dumping Everything Into 'Other' — Field Notes on Catching Silent Classification Drift
An AI expense classifier in a Rork finance app can be accurate at launch and quietly decay over months until the monthly advice goes wrong. Here is how I instrumented confidence and category distribution to get ahead of the drift, with working code.
AI Models2026-05-05
A Prompt Design Guide for Getting Production-Ready UI from Rork's AI
Learn how Rork's AI interprets prompts and how to craft them so that forms, lists, and cards come out the way you actually intended — with less manual cleanup afterward.
AI Models2026-04-30
Building an AI-Powered Photo Organizer App with Rork — A Complete Implementation Guide Using Vision, CLIP, and pgvector
A complete implementation guide for building an AI-powered photo organizer app with Rork. Extract faces, objects, and text with the Vision framework, generate semantic embeddings with CLIP, and run similarity search at scale using Supabase pgvector — designed as a production pipeline from day one.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →