Problem & Constraints
I built Pantas because Civil Service Exam and UPCAT preparation in the Philippines still relies on bulky 500-page printed reviewers or web apps that break when mobile data drops during long jeepney and bus commutes. Commercial review centers charge upwards of ₱10,000, while cheap digital reviewers are often bloated with synthetic passing probabilities and paywalled basic explanations. I wanted to give reviewees a guaranteed offline study tool that accurately tracks memory decay without consuming expensive mobile data buckets.
How It's Built
1. Editorial "Ink & Rule" Design System & Source Sans 3
I stripped away the generic AI aesthetic (Poppins bold, stock blue #1F6BFF, drop shadows, emoji icons, pastel pills) in favor of an editorial printed-workbook identity. I used warm paper surfaces (#FBF9F4), near-black ink (#26221B), 1px hairline rules (#E5DFD3), and tabular figures, budgeting Board Green (#1D5C50) strictly for the single primary CTA per screen. I originally tried pairing a serif with sans, but on a phone screen the serif became an eyesore at small sizes and distracted from the content. I standardized on Source Sans 3 across all 15 typography roles to keep the focus entirely on reading.
Trade-off:Abandoning standard Material cards and elevated shadows meant writing custom layout math with hairline dividers, but it eliminated visual fatigue during 2-hour study drills.
2. Local-First Encrypted Persistence via Drift SQLite & SQLCipher
I designed the app under one core rule: studying is never blocked by the network; only account and money are. All question banks, user response logs, and scheduled drill states live in an encrypted local database using Drift SQLite with 256-bit AES SQLCipher for RA 10173 compliance. I replaced dynamic runtime CMS queries with an immutable, pre-compiled static SQLite seed (content.db / assets/seed/v1.json), ensuring cold boots and drill queries stay instant.
Trade-off:Bundling static seed databases adds ~8MB to the initial APK size, but guarantees zero-latency drills with 100% offline availability.
// Pure local database setup with SQLCipher encryption and static seed loader
LazyDatabase openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'pantas_encrypted.db'));
return SqlcipherDatabase(
file,
password: await secureStorage.getDatabaseKey(),
setup: (rawDb) => rawDb.execute('PRAGMA cipher_memory_security = OFF;'),
);
});
}3. Pure Dart FSRS-6 Spaced Repetition Engine & On-Device Optimizer
I implemented the FSRS-6 algorithm locally in pure Dart using the 21-parameter weight vector with dedicated same-day stability formulas for exam cramming behavior. To personalize intervals without sending study logs to a cloud server, I designed an on-device optimizer that fits parameters directly on device once a student logs 200 reviews (compared to Anki's 400+ threshold), guarded by a held-out test split to prevent overfitting.
Trade-off:FSRS-6's 200-review optimizer threshold is calibrated for short 8-week Philippine exam countdowns, using held-out split validation to reject overfitted weights while allowing students to revert to defaults in Settings.
// Pure Dart FSRS-6 scheduler with 21-parameter weights and same-day stability
FsrsItem scheduleFsrs6Review(FsrsItem item, Rating rating, DateTime now, List<double> w) {
final elapsedDays = item.lastReviewed == null ? 0.0 : now.difference(item.lastReviewed!).inHours / 24.0;
final nextStability = elapsedDays < 1.0
? fsrs6.calculateSameDayStability(item.stability, rating, w)
: fsrs6.calculateStability(item.stability, item.difficulty, elapsedDays, rating, w);
final nextDifficulty = fsrs6.calculateDifficulty(item.difficulty, rating, w);
final intervalDays = fsrs6.nextInterval(nextStability, targetRetention: 0.90, decay: -w[20]);
return item.copyWith(
stability: nextStability,
difficulty: nextDifficulty,
due: now.add(Duration(days: intervalDays.clamp(1, 36500).round())),
lastReviewed: now,
);
}4. Assessment Hub & Distractor Misconception Explanations
I replaced generic topic queues with an Assessment Hub (Today's Session, Drill, Retake, Mock Exam). Instead of showing a simple green checkmark or red cross, I authored answer reveals that explicitly explain why each incorrect choice (distractor) is wrong. To recreate real exam pressure, I wrote a custom canvas OMR bubble sheet with strict section boundary timers, question jump grids, and blueprint-weighted subject ratios.
Trade-off:Authoring custom misconception explanations for all 4 multiple-choice options quadrupled content writing time, but prevented students from relying on rote memorization.
5. Psychometric Integrity ("Never Invent a Figure")
I instituted a strict rule: never invent a figure. I banned synthetic score predictions and fake readiness percentages ('Passing Probability: 92%'). Instead, I structured the Progress tab around four honest questions: The Diagnosis (where points leak and why), The Mirror (behavioral archetypes like stamina drop-off and pacing under pressure), The Record (measurable movement over time), and What's Fading (FSRS decay curves). I show score impact strictly as point deltas ('Fixing these weak topics is worth +9 points').
Hurdles & Solutions
SQLCipher Database Migration Deadlocks on Budget Devices
Problem:When I ran question bank migrations and schema updates during cold boot on low-RAM Android devices, the SQLite database locked up and threw unhandled exceptions before the home view could mount.
Resolution:I decoupled static question banks from mutable user response tables and moved schema migrations into a background isolate with a dedicated write-ahead log (WAL) pool, unblocking the main UI thread.
FSRS-4.5 Cramming Flaws & Same-Day Review Drift
Problem:In FSRS-4.5, repeating the same card multiple times during intense last-minute cram sessions had no stability formula, causing intervals to distort and easy cards to bury high-yield civil service and UPCAT topics.
Resolution:I upgraded to FSRS-6's 21-parameter weight vector with dedicated same-day review stability calculations (w[17..19]) and trainable decay. I built a local Dart optimizer with a 200-review threshold and held-out validation guard, giving cramming reviewees accurate intervals without cloud dependencies.
Cold-Start Entitlement Race Conditions in Offline Posture
Problem:Standard subscription SDKs fail closed when network requests time out. A student studying on an offline commute could lose Pro access if a check failed.
Resolution:I instituted a fail-open local cache rule: the last known entitlement state stands until positively contradicted by a successful server verification. Cached subscription tokens survive cold starts and are read before the first frame renders.
Results & Numbers
I delivered sub-15ms local query performance across 2,216+ question bank items with 100% offline study operation. The app runs without network dependencies during drills, eliminates synthetic passing metrics, and complies with RA 10173 on-device data encryption.