A spaced-repetition study PWA built to help you ace any certification exam — flashcards, practice quizzes, and concept review. It ships with a complete CompTIA A+ Core 2 (220-1202) question bank, and its multi-exam architecture means the next course (Network+, Security+, or anything else) is just another dataset to drop in. Runs in any modern browser, works fully offline, and installs to the home screen on desktop, Android, iPhone, and iPad. Built neurodivergent-first, with extra polish on iPad (an Apple-Pencil scratch pad) and iPhone (a thumb-friendly layout with swipe-to-advance). All questions are original, and it's open to fork.
⚠️ Unofficial, independent project — not affiliated with, authorized, or endorsed by CompTIA. All questions are original, written from the publicly published exam objectives; no real exam content is reproduced. See Legal and disclaimer. “CompTIA” and “CompTIA A+” are trademarks of CompTIA, used here only to name the exam this app currently includes a question bank for. (“A+” in the app's name is about acing the exam — bringing your A-game — not a literal grade.)
🔗 Live demo: aplusstudyapp.pages.dev — installable; works fully offline once loaded.
A spaced-repetition study app is a common project. The parts I think are worth a look:
- A real spaced-repetition engine, built from scratch. The scheduler is a
hand-written implementation of FSRS-4 (Free Spaced Repetition Scheduler —
the algorithm Anki adopted as its
default). It models each
card's memory as a stability + difficulty pair and schedules the next
review to hit a target retention, and it's exam-aware — intervals contract
as the test date approaches. Pure and unit-tested, in
lib.mjs. - Zero runtime dependencies, no build step. ~6,700 lines of vanilla ES-module
JavaScript, split into focused modules with a one-way dependency graph:
core.mjs(shared state + DOM/dialog primitives),lib.mjs(pure, tested SRS/formatting/URL-safety helpers),crypto.mjs(encryption),storage.mjs(IndexedDB + at-rest encryption),sync.mjs(optional Supabase push/pull), and self-contained feature modules (read-aloud,scratchpad,shake,image-zoom,wake-lock,confetti,focus-sound) that theapp.jsview layer wires together — no framework, no bundler, nonode_modulesat runtime. Clone and openindex.html. - Offline-first PWA. A service worker precaches the shell + question bank, so after the first load it runs in airplane mode and installs to the home screen like a native app — on desktop, Android, and iOS/iPadOS alike.
- Real client-side encryption. Optional PIN lock derives an AES-GCM-256 key
via PBKDF2 (SHA-256, 600,000 iterations, random salt) and re-encrypts
everything in IndexedDB at rest. The PIN and derived key are never stored —
only a salt and a verification blob (
crypto.mjs). - A genuine touch-safety problem, solved. iPad "ghost taps" after revealing
an answer would skip cards. The fix is a deliberate 5-layer stack (CSS
pointer-eventslockout, JS timestamp guards, swipe-target checks, scoped sticky positioning, ID-based history) — documented inAGENTS.mdso it doesn't regress. - Accessibility as a design driver, not a checkbox. Focus traps, skip links,
prefers-reduced-motionsupport, dyslexia-friendly fonts (Atkinson Hyperlegible, OpenDyslexic), high-contrast mode, full text-scaling, and an "anxiety mode" that hides judgemental metrics. See the AuDHD-friendly features section. - Tested + CI-gated. 77 unit/data tests via Node's built-in test runner,
plus a content validator (
scripts/validate-questions.mjs) that catches unwinnable questions, mismatched objectives, and broken image refs. Both run in GitHub Actions on every push.
| Layer | Choice |
|---|---|
| Language | Vanilla JavaScript (ES modules), HTML, CSS — no framework |
| Scheduling | FSRS-4 spaced-repetition algorithm — custom implementation (lib.mjs) |
| Storage | IndexedDB (progress, per-question overrides, scratchpad drawings, reference PDFs) + localStorage for prefs |
| Offline | Service Worker (cache-first precache) + Web App Manifest |
| Crypto | Web Crypto API — PBKDF2 → AES-GCM-256 |
| Optional sync | Supabase (anon key + user-chosen sync key) |
| Tests | node --test (77 tests) + a custom JSON content validator |
| CI / hosting | GitHub Actions → Cloudflare Pages |
Calm / low-stimulation modes — for the overstimulated (autism-side) days: a dark theme, and Focus Mode, which strips away the tab bar, filter chips, progress counters, and card meta to leave just the question and answers.
- Study mode — flashcards (474 questions across the Core 2 220-1202 objectives) with spaced repetition (again / hard / good / easy). Cards come back at increasing intervals based on how well you did; "again" brings it back in a minute, "easy" pushes it out days.
- Quiz mode — same questions but tracked as right/wrong for accuracy stats. Wrong answers are scheduled for quick review; right answers graduate out.
- Reading mode — 37 concept-fix sheets, one per Core 2 sub-objective, plus a malware-removal mnemonics sheet and the 6-step troubleshooting methodology. Tables, code, and "For the exam" tips for each topic; navigable from a sticky TOC sidebar.
- Stats mode — mastery bars per OBJ, accuracy, shuffle toggle, export/import progress, reset.
- Readable explanations — long CompTIA explanations are auto-split into a lead answer + supporting paragraphs, with any "For the exam..." tip pulled into its own callout. No more walls of text.
- Apple Pencil scratch pad — beneath every question on iPad (shown at widths >600px), pressure-sensitive canvas for subnet math, diagrams, etc. Hidden on iPhone portrait to keep the card readable.
- Filter by OBJ, "Due", or search — scroll the filter bar to drill a specific objective, toggle the green Due (N) chip to see only cards scheduled for review, or type in the search box to narrow by question/explanation text.
- Shuffle — optional random order, toggled from Stats. Persists across sessions.
- Swipe / keyboard / prev — swipe left on Study/Quiz cards to skip; use the ← Prev button to go back; desktop keyboard shortcuts (see below).
- Theme toggle — 🌓 button in the header cycles Auto / Light / Dark, saved to your device.
- Export / import progress — download your progress as JSON from Stats, import it on another device or after a browser wipe.
- Offline — service worker caches everything. Once installed, works in airplane mode.
- Progress persists — IndexedDB stores ratings, FSRS stability/difficulty, and next-due timestamps between sessions.
| Key | Action |
|---|---|
Space / Enter / R |
Reveal answer. If already revealed: Study mode advances with a "good" rating; Quiz mode just skips (explicit right/wrong tap is required to record a quiz result). |
1 / 2 / 3 / 4 |
Rate: Again / Hard / Good / Easy (Study mode, after reveal) |
→ / K / N |
Next question |
← / J / P |
Previous question |
T |
Cycle theme (auto / light / dark) |
F |
Toggle Focus Mode (hides chrome) — Esc also exits |
Built to be flexible, because sensory needs flip between understimulated (ADHD-side: needs visual engagement) and overstimulated (autism-side: needs calm, minimal UI). Everything here is togglable from Stats → Accessibility, Stats → Focus session, and the 🔒 / 🌓 header buttons.
- Focus Mode (🔒 button or
F) — hides the tab bar, filter chips, search box, progress HUD, and card meta tags. Just the question. Great when scrolling chrome becomes noise. - Focus Sessions (Stats → Focus session):
- Time-boxed — 5 / 15 / 25 min with a visible ⏱ countdown in the header (time-blindness).
- Card-count micro-goals — 1 / 3 / 5 / 10 cards. Session ends automatically when the count is hit. "One card" is a valid commitment; you can always do one.
- End-of-session summary celebrates whatever you did. "End now" exits early without guilt.
- Anxiety Mode — hides accuracy %, progress counters, mastery bars, seen counts. Keeps streak + session timer. Turn on when numbers feel like judgement.
- Focus sound — built-in white / pink / brown noise generator via Web Audio (no downloads, no tracking). Pink is gentler than white; brown is "the one that sounds like a waterfall."
- Shake to shuffle — iPhone only. Toggle in Accessibility, grant motion permission when prompted, then shake the phone to flip shuffle on/off mid-study (with a haptic confirmation).
- Text size — S / M / L / XL. Scales the whole app.
- Font — System default, Atkinson Hyperlegible (open-source, designed for low vision), or OpenDyslexic (weighted letter bottoms to resist letter-swapping). The two opt-in fonts load on demand from their web-font CDNs (Google Fonts and cdnfonts); the default path makes no external requests.
- High contrast — pure-black background + brighter text/borders. Reduces visual clutter.
- Reduce motion — kills transitions and animations. The OS-level
prefers-reduced-motionsetting is also respected automatically. - Haptic feedback — on by default (a tiny tap on every rate). Toggle off if vibrations are distracting.
- Daily streak + Today counter in Stats — dopamine-friendly "I did a thing" signal without a full leaderboard grind.
- Scratch pad — doubles as a drawing / fidget space on iPad while you think. Hidden on iPhone portrait to cut clutter.
- Auto-sync — if you've set up Supabase, flip "Auto-sync" on and every rated card quietly syncs 5s later. No "did I forget to push?" worry.
Design principles that shaped this:
- Everything is a toggle, nothing is a mandate. Today you might want haptics + motion + high-contrast; tomorrow you might not. Preferences persist per device.
- Reduce decision load. The "default next action" (Reveal, Skip, a rating button) is always visually primary, always in the same spot.
- Time is visible. Session countdown + card progress + due count are all numeric — no guessing "how long have I been at this?"
- Low-stakes sessions. You can start a 5-minute session. You can end it early. Rating one card counts as "showing up."
None of this is medical advice — it's just options that map to patterns in the neurodivergent design literature. Use what helps, ignore what doesn't.
Just open aplusstudyapp.pages.dev. No account, no install, nothing to download — it runs in any modern browser and works fully offline after the first load.
Add it to your home screen (optional):
- iPhone / iPad: open the link in Safari → Share → Add to Home Screen. Opens full-screen like a native app, with full Apple-Pencil support on iPad.
- Android / desktop (Chrome or Edge): tap the install icon in the address bar.
First run: tap Start studying → read the card → Reveal → rate how it felt (Again / Hard / Good / Easy). It schedules each card's next review from there. Come back any time and tap the green Due chip to review just what's scheduled.
Progress is saved on your device. To move it elsewhere, use Stats → Export / Import, or set up optional cloud sync.
Everything below is for forking, self-hosting, or tweaking the app — you don't need any of it to just use it at aplusstudyapp.pages.dev.
The app is a static site with no build step and zero runtime dependencies —
clone the repo and open index.html, or serve it for full offline /
service-worker behavior:
npm run serve # python3 -m http.server 8000 → http://localhost:8000
npm test # 77 unit + data + crypto tests (node --test)
npm run validate # validate data/core2/questions.json
npm run check # syntax-check the JS sourcesNo npm install is needed to run the app (zero runtime deps); the browser smoke
suite uses puppeteer, a dev-only dependency. The live site deploys to
Cloudflare Pages from main via GitHub Actions — but any static host works.
Two ways to fill in or fix a card's options/image:
Every Study/Quiz card now has a small ✏️ Edit button in its meta row. Tap it to open a form where you can:
- Paste the four MC options (one per line)
- Add an image URL (
images/p1q36.png, or any HTTPS URL)
Saves are stored in IndexedDB as overrides — they don't touch
data/core2/questions.json. An "✏️ Edited" tag appears on cards you've edited
so you can see your work. Stats → Question edits → Export dumps your
overrides as JSON; Import loads them back. They sync via cloud too (see
below).
This is the fastest path: open a card, type the four options, save, move on.
If you want the options/images committed for everyone (or you have many to add
at once), edit data/core2/questions.json. Each entry is an object:
options— an array of strings. When present, they're rendered as a lettered list (A, B, C, D) above the Reveal button. Absent = the old behavior (think-then-reveal). The app doesn't score clicks on options; you still self-rate.image/images— paths relative to the project root. Drop PNG/JPG into animages/folder and reference it here. PBQs without an image show a yellow "image not available" banner so you can still read the explanation.
In-app edits (Option A above) live in IndexedDB and are merged onto the base question at render time, so an in-app edit overrides the JSON for that question.
Optional. Lets your devices share progress and edits without exporting JSON manually.
- Create a free Supabase project at https://supabase.com.
- In the SQL editor, run the script below (it's also in
docs/supabase-sync-hardening.sql, and the app's Sync & backup dialog shows the same script with a copy button). The table has no anon policies — it's reachable only through twoSECURITY DEFINERfunctions that each require your sync key, so the public anon key can't read or overwrite anyone else's row:
create table if not exists public.progress (
sync_key text primary key,
data jsonb not null,
updated_at timestamptz default now()
);
alter table public.progress enable row level security;
-- No anon policies on purpose: the table is reachable ONLY through the
-- two functions below, and each one requires your sync key.
create or replace function public.progress_pull(p_sync_key text)
returns table (data jsonb, updated_at timestamptz)
language sql security definer set search_path = public as $$
select p.data, p.updated_at
from public.progress p
where p.sync_key = p_sync_key;
$$;
create or replace function public.progress_push(p_sync_key text, p_data jsonb)
returns void
language sql security definer set search_path = public as $$
insert into public.progress (sync_key, data, updated_at)
values (p_sync_key, p_data, now())
on conflict (sync_key) do update
set data = excluded.data, updated_at = excluded.updated_at;
$$;
grant execute on function public.progress_pull(text) to anon;
grant execute on function public.progress_push(text, jsonb) to anon;- In Settings → API, copy:
- Project URL (
https://xxxx.supabase.co) - anon / public key (the long
eyJ…JWT)
- Project URL (
- Open the app → Stats → Cloud sync (Supabase)
- Paste the URL, the anon key, and pick a Sync key — any string you want,
must be the same on every device (e.g.
my-aplus-2026). - Tap Save.
- ⬆ Push — sends your local progress + question edits to the cloud through
the
progress_pushRPC, replacing whatever was stored for your sync key. - ⬇ Pull — fetches the cloud copy through
progress_pulland merges it in, per-card last-write-wins (whichever side has the newerupdated_at).
Workflow: study on iPad → Push. Open iPhone → Pull. Study on iPhone → Push.
The table is locked down — no anon policies — and reachable only through the
two SECURITY DEFINER functions, each of which requires your sync key, so
the public anon key alone can't read the table or overwrite anyone's row. That
makes your sync key the real secret: anyone who has your project URL, anon key,
and sync key can read/write your row, so pick a long, non-obvious sync key and
keep it private. (It's still good practice not to commit your anon key to a
public repo.)
Optional. Stops a curious family member (or a pickpocket) from opening the app and reading your progress.
- Stats → App lock → Set PIN. Pick a PIN of 4+ characters, re-enter to confirm.
- The app derives an AES-GCM 256 key from the PIN via PBKDF2 (SHA-256, 600,000 iterations, random salt) and immediately re-encrypts your existing progress, question edits, and scratchpad drawings under that key.
- The salt + a "can you decrypt this sentinel?" verification blob are saved to localStorage. The PIN and the derived key are never stored.
- ✅ At rest, the contents of IndexedDB are ciphertext. Opening DevTools and
browsing the
aplus-studydatabase shows random-looking blobs, not your study history. - ✅ Losing the device to casual hands means they hit the lock screen and can't decrypt.
- ❌ If the attacker has the device, knows your PIN, and unlocks the app, everything decrypts.
- ❌ Supabase cloud sync isn't affected — cloud data is still stored under the anon key + sync_key only. If you need encrypted cloud sync, that's a future extension.
- Every time you launch the app, the lock screen asks for your PIN. The derived key lives in memory only — closing the app drops it.
- Forgot PIN? The lock screen offers "Wipe local data" — it clears the encrypted stores and the setup meta, so you can start over (or re-pull from Supabase on another device that still has a working copy).
- Change PIN (Stats → App lock → Change) re-encrypts every stored blob under a new key without dropping data.
- Remove PIN (Stats → App lock → Remove) decrypts back to plaintext. Use this if you don't want the lock anymore.
If you set a PIN on iPad, you'll need to set one on iPhone independently — each device has its own encrypted store. Supabase pull still works because cloud blobs are plaintext.
The app supports multiple exam datasets side by side. Core 2 (220-1202) ships populated.
data/
└── core2/
├── questions.json # Core 2 questions
└── concept-fixes.json # Core 2 concept-fix sheets
Edit the EXAMS map near the top of app.js — add { id, label, questions, fixes } — drop files under data/<id>/, and the Stats switcher will pick it up
automatically. CI's validate-questions.mjs accepts any path, so add a step to
.github/workflows/ci.yml for the new file.
- Stats → Active exam — toggle picks the active dataset.
- Each exam has independent progress, question overrides, and scratchpad drawings. Switching preserves both sides.
- PIN lock, if enabled, encrypts progress for every exam — no extra setup needed.
- Cloud sync (Supabase) bundles every exam's progress + overrides in a single row (payload v2).
- Streak + focus sessions are global, not per-exam.
The structure, if you want to fork and add to it:
studyapp/
├── index.html # Three-tab shell
├── styles.css # iPad-first dark/light auto
├── app.js # View layer — render, state, router; wires the modules below
├── core.mjs # Shared state + DOM/dialog primitives (imports nothing)
├── lib.mjs # Pure, tested SRS + formatting + URL-safety helpers
├── crypto.mjs # PBKDF2 → AES-GCM-256
├── storage.mjs # IndexedDB + at-rest encryption
├── sync.mjs # Optional Supabase push/pull
├── read-aloud.mjs, scratchpad.mjs, shake.mjs, image-zoom.mjs,
│ wake-lock.mjs, confetti.mjs, focus-sound.mjs # self-contained features
├── manifest.json # PWA install config
├── sw.js # Service worker (offline cache)
├── data/
│ └── core2/
│ ├── questions.json # Core 2 questions
│ └── concept-fixes.json # Per-OBJ fix sheets as HTML strings
└── icons/
├── icon-180.png # Apple touch icon
├── icon-192.png # Web manifest
└── icon-512.png # Web manifest (high-res)
All questions are original, written against CompTIA's publicly published exam objectives — no real exam content, and nothing copied from anyone's practice tests. (See Legal and disclaimer.) To add your own:
# 1. Append objects to data/core2/questions.json (schema in docs/DATA-FORMAT.md).
# Each needs a unique id, an obj, the question, options, and the answer.
# 2. Dedupe / normalize across the bank
node scripts/dedupe.mjs
# 3. Validate (catches unwinnable questions, mismatched objectives, etc.)
node scripts/validate-questions.mjs --allWrite questions in your own words. The fastest safe workflow is to pick an objective, study the underlying concept, and author a fresh scenario that tests it — the same thing any third-party study guide does.
- Audio explanations: Use the Web Speech API (
speechSynthesis) to read out the correct answer when revealed. - Search: Add a search box that filters on question text.
- Export to Anki: Convert
questions.json→ Anki.apkgviagenankiPython library. - Auto-sync: the current Supabase integration is manual push/pull. Could
call
cloudPush()on every save (debounced) for true auto-sync. - Tune the SRS: the scheduler lives in
schedule()inlib.mjs— cap is 30 days so exam-prep doesn't schedule past the exam. ChangeMAX_INTERVAL_DAYS(also inlib.mjs) if you want longer intervals after the test.
- Storage is sandboxed per-origin. If you re-deploy to a new URL, you lose progress. Keep the same deployment URL for a given study cycle.
- Safari may evict PWA storage if the app hasn't been opened in ~30 days and the device is low on space. Low risk in normal use.
- Push notifications require iOS 16.4+ AND the app must be installed to home screen first. Not currently wired up.
- No
localStorage/sessionStoragequota issues — this app uses IndexedDB which has much higher limits (~500 MB).
- No Performance-Based Questions (PBQs) yet. The real Core 2 exam includes a
few image / simulation PBQs (motherboard diagrams, router admin screens,
etc.). The renderer is image-ready (drop a question with
qtype: "PBQ"andimage:/images:path and it just works), but the current bank doesn't include any yet. - Interactive (drag/order/match) PBQs aren't supported. PBQ support is image
- multiple-choice; drag-to-reorder interactions are out of scope.
- Cross-device sync is manual. iPhone and iPad keep separate progress unless you wire up Supabase cloud sync (see the developer section) and tap Push/Pull. Stats → Export/Import works as a no-backend alternative.
The in-app Send feedback button (Help → Send feedback) collects the message
plus optional diagnostics (current screen, app version, device). Where it goes
depends on one config value in app.js:
FEEDBACK_FORM_KEYis blank: opens a prefilled GitHub issue — works out of the box, no email exposed, but submitting needs a GitHub login.FEEDBACK_FORM_KEYis set: posts the report to Web3Forms, which emails it straight to you — no login required, and your email address never appears in the source.
To turn on direct-to-inbox feedback (~1 minute):
- Go to web3forms.com, enter the email you want reports sent to, and copy the access key they email you.
- Paste it into
const FEEDBACK_FORM_KEY = '...'near the top of the feedback section inapp.js.
The access key is a public, submit-only token — safe to commit. It can only post to your form and can't read anything, and your actual email stays on Web3Forms, not in this repository. (If a report is too long for a Web3Forms delivery it falls back to copy-to-clipboard.)
This is an independent, unofficial study aid shared as a personal portfolio project. It is not affiliated with, sponsored, authorized, or endorsed by CompTIA, and it is not an official CompTIA product.
- Trademarks. "CompTIA" and "CompTIA A+" are trademarks (or registered trademarks) of the Computing Technology Industry Association (CompTIA). They are used here only nominatively — i.e. to truthfully name the certification exam this app currently includes a question bank for. "A+" in the app's own name is about acing the exam — bringing your A-game — not a literal grade (the app is a general study tool, with more exams planned). No claim is made to any CompTIA mark, and no affiliation or endorsement is implied.
- Questions are original. Every question in
data/was written from scratch against CompTIA's publicly published exam objectives, which list the topics the exam covers. Exam objectives are facts about the test; the questions that teach those topics are this project's own wording. No actual CompTIA exam questions, official practice-test items, or other copyrighted question banks are reproduced, included, or derived here. That's the same boundary every legitimate third-party study guide operates within. - No copyrighted study materials are bundled. Any reference PDF/book feature
stores files only on your own device (in-browser IndexedDB) and never in
this repository —
.gitignoreblocks*.pdf,*.epub,*.docx, etc. - Code license: MIT. The code is free to use, fork, and modify.
- No warranty / not exam advice. Provided "as is", with no guarantee of accuracy or completeness, and no guarantee you'll pass any exam. Always verify against current official CompTIA materials.
If you are a rights holder and believe something here is mistaken, please open an issue and it will be addressed promptly.





{ "id": "c2q3", "obj": "2.5", "qtype": "Multiple Choice", // or "Multiple Answer" or "PBQ" "question": "Which of...", "correct_short": "Cable modem", "explanation": "OBJ 2.5: ...", // Optional — add these to enhance a card: "options": ["Cable modem", "DSL", "ONT", "SDN"], // shown above the Reveal button "image": "images/c2q3.png", // single figure "images": ["images/c2q3-a.png", "images/c2q3-b.png"] // or multiple }