Skip to content

Latest commit

 

History

824 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FlashLearnAI.WitUS.Online

AI-powered flashcard platform with spaced repetition, versus mode, and offline-first architecture.

Live: flashlearnai.witus.online

Features

  • AI Generation. Create flashcards from topics, PDFs, YouTube videos, audio files, and images (OCR).
  • Spaced Repetition. SM-2 algorithm schedules reviews at optimal intervals.
  • 3 Study Modes. Classic flip cards, multiple choice, type-your-answer with AI grading.
  • Rich cards. Authored multiple-choice options (scored by option id) and images or video on either side with alt text, so a set can ask "identify the image" questions. Set owners attach card images from My Flashcards (the photo button on a set opens a per-card editor); partners set them through the API. Both paths upload through the same Cloudinary-backed helper with the same 10MB image cap, and either can point at a partner CDN URL instead. Alt text is required on every uploaded image, because the study player reads it out.
  • Curated math library. Repo-authored sets, loaded by npm run seed:math: every single-digit addition, subtraction, multiplication, and division fact, split into small sets so a student can drill one number at a time, plus geometry, trigonometry, and calculus reference sets. Fact cards carry authored multiple-choice answers, so math practice costs no AI generations.
  • Your library. A personal shelf of the sets you actually use, and the first thing on the dashboard when you sign in. Add any public set from Explore or from a set's own page, and anything you create lands there automatically. The shelf points at the set rather than copying it, so corrections to a public set reach everyone who keeps it, and removing a set leaves your progress alone: add it back and the streak picks up where it left off. Sorted by what you studied most recently, then by what you added most recently. No limit on how many sets you keep.
  • Set ratings. Signed-in learners rate any public set one to five stars. A rating is one document per person per set, so changing your mind updates your existing star instead of stacking a second vote, and the running average and rater count live on the set so Explore can sort by "Highest rated" without a per-set query. Authors cannot rate their own sets.
  • Versus Mode. Head-to-head challenges with composite scoring (accuracy, speed, confidence, streaks) and ELO ratings.
  • Offline study. Sets you own are copied to a local SQLite store on app start, on reconnect, and every five minutes, so you can study them with no connection. Card images, alt text, video, and authored answer choices come down with them. Results are held in IndexedDB and upload when the connection returns, which is when the spaced-repetition schedule advances. The copy runs one way, server to device: sets edited offline and public sets you do not own are not covered.
  • Teams & Classrooms. Study groups with join codes, shared sets, team chat, and teacher-led classrooms. When the owner deletes their account, the group or classroom is archived rather than removed: it stays in members' listings marked as archived, members keep reading and studying what is already there, a banner at the top of the page says why, and every write is refused with a 409 until an admin reassigns it. Leaving and deleting still work.
  • Teacher-managed student accounts. A teacher adds a student to a classroom by name and nothing else: no email address, no signup, no second device. The student gets a real account, so study sessions, card results, achievements, and the SM-2 schedule attach to them exactly as they would for anyone who signed themselves up. The account carries no password and is refused at both sign-in paths, and its address sits in the reserved .invalid TLD (RFC 2606), so it can never resolve or receive mail. Creating the student returns a claim code shown once. The student later enters that code with their own email and password, and the account becomes theirs keeping every session and every review date, because the user id never changes. A teacher can only manage students in a classroom they teach, checked against the classroom rather than the role, and cannot set a student's password anywhere: a teacher who could would be indistinguishable from the student in the proctoring audit trail. Removing a student from a roster unenrols them and leaves the account alone. The roster lives on the classroom page, where Start session on a student's row opens study with that student already chosen and says so before a set is picked. Students claim their account at /claim, which needs no sign-in because they have never had one. The routes live under /api/teacher and are authenticated by browser session, so they are app routes rather than part of the public v1 API and do not appear in the OpenAPI spec. Adding students one at a time is the whole of it; there is no bulk import, and managed students have no avatar or PIN sign-in.
  • Student progress, for the adult responsible for it. Progress on a roster row opens one student's record: accuracy all time and over the last 30 days, cards right and cards wrong, sessions finished with the date each one ran, time studied, a per-set breakdown with the weakest set first, and every card the student has answered wrong at least once. Who may read a student's numbers is decided by lib/study/resolveStudySubject.ts, the same module that decides who may run a study session for that student, so the two answers cannot drift apart: an active classroom you teach, or a student linked to your account. A student who has never finished a session gets "No study sessions yet" and a stated absence of accuracy rather than 0%, because a zero reads as having answered everything wrong. The response carries no email address at all, so a managed student's placeholder address cannot reach the page. Two figures differ from the student's own view on purpose and say so on screen: headline accuracy is correct over attempted, so it multiplies out against the counts beside it, and a card is listed as one to go over after a single miss rather than two. A parent or guardian reaches these pages and nothing else under /teacher; classrooms and assignments stay with the teaching roles, and there is no separate family area. The guardian link itself is created by an admin on /admin/users, which is the only way one gets made: a parent-initiated or teacher-initiated version needs a consent flow that does not exist yet. The page is /teacher/students/:studentId/analytics and it reads GET /api/teacher/students/:studentId/analytics, both authenticated by browser session rather than an API key, so neither appears in the OpenAPI spec.
  • Sign in with WitUS. Single sign-on against the ecosystem identity provider at accounts.witus.online. An SSO sign-in resolves to a real FlashLearnAI account: the address is looked up locally, an account is created when there is none, and the session carries that account's id, stored role, and subscription tier, so signing in this way never changes what someone is. Linking an SSO identity to an account that already exists happens only when the identity provider asserts the address is verified, and a missing claim counts as unverified, because auto-linking an unverified address to a password account is an account-takeover path. Managed student accounts and suspended accounts are refused. A refused sign-in shows one message whatever the reason; the reason goes to the server log only, since a message per reason would tell anyone holding an address what kind of account it is here. The provider is registered only when WITUS_OIDC_CLIENT_ID is set, and the button renders only when NEXT_PUBLIC_WITUS_SSO=true.
  • "Continue as ". The sign-in page renders immediately as it always has and, in parallel, asks the identity provider whether this browser already holds a WitUS session. If it answers, the "Sign in with WitUS" button relabels to "Continue as " so nobody types an email they have already proved. If it does not answer — a timeout, a CORS refusal, or a browser that partitions third-party cookies, which is Safari and Firefox and therefore the common case — nothing changes and nothing is said, because an error about a check the visitor never asked for is worse than no check. The name is display copy and never a credential: it arrives in a cross-origin response, so it is stripped of control characters, trimmed and capped at 48 characters before rendering, and clicking still runs the full OIDC code flow, which is the only thing that establishes identity. A one-shot sessionStorage marker written before the redirect, plus ?sso=tried and NextAuth's own ?error= read off the URL, stop a stale identity-provider session from bouncing someone between the two pages forever. The check runs only when WITUS_OIDC_CLIENT_ID is set, since an offer that cannot be completed is worse than none.
  • Global sign-out. Signing out of FlashLearnAI signs you out of every WitUS app in that browser. The local session is destroyed first and the identity provider is handed the browser second, so an unreachable or refusing identity provider still leaves the person signed out here rather than producing "I clicked sign out and I'm still signed in." The button reads "Sign out of WitUS" when it reaches the ecosystem and plain "Sign out" when WITUS_OIDC_CLIENT_ID is unset and sign-out is purely local.
  • Public API. A REST API for building on top of FlashLearnAI, including card media upload and per-student progress for partners.
  • Ecosystem API for cross-product partners. Spaced-repetition and comprehension backend for any consumer-facing learning product. Learner-scoped scheduled sessions, per-standard mastery rollups, cascade-delete, and signed outbound webhooks. Powers Wanderlearn and BVC classes.
  • Signed outbound webhooks. HMAC-SHA256 signed callbacks with 7-attempt exponential backoff, dead-letter, AES-256-GCM secret encryption at rest, and a self-service developer dashboard with replay.
  • White-label app. Branded study platform for schools and companies (sold separately).
  • Marketing & link tracking. Switchy.io short links with pixel attribution on all shared content.
  • Admin dashboard. Revenue analytics, user management, content moderation, promo campaigns, SEO tools.

Tech Stack

  • Framework: Next.js 15 (App Router), React 19, TypeScript
  • Database: MongoDB Atlas, Mongoose
  • Auth: NextAuth.js with JWT sessions
  • Payments: Stripe (subscriptions + metered billing)
  • Email: Mailgun, Resend
  • AI: Switchable provider layer (lib/ai/) via the Vercel AI SDK. LLM_PROVIDER selects the text backend: Cerebras (default), OpenRouter, Mistral, Together, or Google Gemini. Image generation uses a vision provider (default Mistral mistral-small); audio stays on Gemini.
  • Offline: PowerSync (SQLite via wa-sqlite) as a local read cache, IndexedDB for study results and the upload queue
  • Rate Limiting: Upstash Redis
  • Background Jobs: Upstash QStash (delayed delivery + webhook retries)
  • Error Monitoring: Better Stack via the Sentry SDK (lib/sentry-scrub.ts scrubs emails, cookies, auth headers, and token-bearing URLs before an event is sent). Inert unless a DSN is set. app/global-error.tsx is the last-resort boundary for errors thrown by the root layout itself: it renders its own <html>/<body>, reports the error, and offers a retry. Keep it dependency free and inline styled, since anything it imports could be the thing that broke.
  • Hosting: Vercel
  • Link Tracking: Switchy.io

Getting Started

Prerequisites

  • Node.js 18+
  • MongoDB database
  • Mailgun account

Setup

git clone https://github.com/dapperAuteur/flashlearn-ai.git
cd flashlearn-ai
cp .env.sample .env.local  # Configure your environment variables
npm install
npm run dev

Open http://localhost:3000.

Key Environment Variables

See .env.sample for the full annotated set. Required minimums for local dev:

MONGODB_URI=                          # MongoDB connection string
NEXTAUTH_SECRET=                      # openssl rand -base64 32
NEXTAUTH_URL=                         # http://localhost:3000
LLM_PROVIDER=cerebras                 # Text AI backend: cerebras | openrouter | mistral | together | gemini
CEREBRAS_API_KEY=                     # Key for the selected LLM_PROVIDER (CEREBRAS_/OPENROUTER_/MISTRAL_/TOGETHER_API_KEY)
LLM_VISION_PROVIDER=mistral           # Provider for image flashcards (text-only providers can't accept images)
GEMINI_API_KEY_PUBLIC=                # Google Gemini key, still required for audio flashcards + as fallback
UPSTASH_REDIS_REST_URL=               # Rate limiting + webhook milestone dedupe
UPSTASH_REDIS_REST_TOKEN=
STRIPE_SECRET_KEY=                    # Stripe secret key
MAILGUN_API_KEY=                      # Mailgun API key
MAILGUN_DOMAIN=                       # Your Mailgun domain
SWITCHY_API_TOKEN=                    # Switchy.io API token
SWITCHY_DOMAIN=                       # Custom short link domain
CRON_SECRET=                          # openssl rand -hex 32 (for Vercel Cron)

Required for ecosystem outbound webhooks and delayed session scheduling:

WEBHOOK_ENCRYPTION_KEY=               # openssl rand -hex 32 (AES-256-GCM key for per-endpoint signing secrets)
UPSTASH_QSTASH_TOKEN=                 # Upstash QStash publishing token
UPSTASH_QSTASH_CURRENT_SIGNING_KEY=   # For verifying QStash callbacks
UPSTASH_QSTASH_NEXT_SIGNING_KEY=      # For zero-downtime signing-key rotation

Optional, for WitUS single sign-on. Leave WITUS_OIDC_CLIENT_ID unset and the provider is never registered with NextAuth, the "Continue as " check never runs, and sign-out stays purely local:

WITUS_OIDC_CLIENT_ID=                 # OIDC client id issued by accounts.witus.online
WITUS_OIDC_CLIENT_SECRET=
WITUS_OIDC_DISCOVERY_URL=             # Defaults to the accounts.witus.online discovery document
NEXT_PUBLIC_WITUS_SSO=false           # Renders the Sign in with WitUS button; read in the browser

Optional, for error monitoring. Leave unset and the SDK never initializes:

SENTRY_DSN=                           # Better Stack ingest DSN (server + edge runtimes)
NEXT_PUBLIC_SENTRY_DSN=               # Same DSN for the browser; must be set at BUILD time (CSP)
SENTRY_ENVIRONMENT=                   # Optional label; defaults to VERCEL_ENV, then NODE_ENV
SENTRY_ORG=                           # Build-time only, for source-map upload (readable traces)
SENTRY_PROJECT=
SENTRY_AUTH_TOKEN=

Health Check

GET /api/health is the endpoint to point an uptime monitor at (Better Stack, Pingdom, etc). Do not monitor the homepage: it can answer 200 from cache while the database is down, so a green check there proves nothing.

Every request pings MongoDB, so the status code reflects the app's critical dependency:

Status Body
200 {"ok":true,"checks":{"db":"ok"}}
503 {"ok":false,"error":"database_unreachable","checks":{"db":"fail"}}

Notes:

  • Public and unauthenticated, and deliberately says nothing else. No version, no env values, no counts, no user data, and never the underlying error (a Mongo failure commonly carries the connection URI including the password), only the fixed database_unreachable token.
  • Never cached (Cache-Control: no-store).
  • Bounded by a 4 second timeout, so a hung database returns 503 quickly instead of hanging the check.
  • Checks the database only. No AI provider or other third-party API is called, so a vendor outage cannot turn the uptime monitor red.

Observability & E2E

Error monitoring (Better Stack via the Sentry SDK, inert unless a DSN is set) is covered under Tech Stack and the optional SENTRY_* block in Key Environment Variables. The rest of the observability story:

Distributed tracing

Traces go to Honeycomb over OTLP via @vercel/otel (otel.config.ts, loaded from instrumentation.ts before the Sentry configs, because whoever registers the global tracer provider first wins, and Sentry is told to stand down via skipOpenTelemetrySetup in sentry.server.config.ts). Service name is flashlearnai.

  • Inert until the key is set. HONEYCOMB_INGEST_API_KEY_SECRET (fallback HONEYCOMB_API_KEY). With neither set, registration is skipped entirely, the same leave-unset-and-nothing-initializes pattern as the Sentry DSN.
  • /api/health spans are dropped at the sampler. Uptime monitors probe it around the clock, and those requests must not spend Honeycomb's free-tier event budget. Everything else is recorded unsampled.

Unit test + type-check CI

.github/workflows/test.yml runs tsc --noEmit and npm test as two named steps on every push and pull request, so a failure says which gate broke. It needs no secrets, database, or env: the suite is self-contained and the few tests that need a value set it themselves.

Kept separate from the e2e gate below because that one triggers on deployment_status, meaning it only fires after Vercel finishes a deploy. Unit tests should fail before a deploy, not after.

Two things worth knowing if you touch jest.config.js:

  • The config is an async function, not a plain object. next/jest hardcodes its own node_modules ignore pattern and only appends yours, and because transformIgnorePatterns is an OR, next/jest's pattern always matches first. Appending an allowlist to it is dead config. We resolve next/jest's config and then replace the array outright, which is what actually gets the ESM-only packages compiled.
  • Add ESM-only packages to the esmPackages array, not to transformIgnorePatterns directly.

E2E + accessibility CI

Playwright specs live in e2e/; the gate runs in .github/workflows/e2e.yml on deployment_status. It tests the real Vercel deployment URL (preview → full suite, production → @smoke only), so CI needs no secrets, database, or env. The suite runs desktop plus a 360px mobile project, and covered pages must pass an axe check with zero serious or critical violations. Minor and moderate findings are reported but don't gate. The gate is strict on purpose; fix the page, not the gate.

  • Local runs: PLAYWRIGHT_BASE_URL=<url> npx playwright test. Local runs drive installed Chrome via channel: "chrome" (Playwright's bundled chromium doesn't support macOS 13); CI uses the bundled browser.
  • If the Vercel project enables Deployment Protection, set the project's "Protection Bypass for Automation" secret as the VERCEL_AUTOMATION_BYPASS_SECRET Actions secret; public previews need nothing.

Synthetic traffic tag

Every request Playwright makes, the CI gate and tutorial recordings alike, carries x-witus-origin-test: playwright-synthetic (an extraHTTPHeaders entry in both Playwright configs). The OTel layer surfaces it as the witus.origin_test span attribute (attributesFromHeaders in otel.config.ts), so Honeycomb queries can include or exclude synthetic traffic. Absent header = attribute absent = real user; queries about real users exclude the attribute.

Tutorial pipeline (tutorial-as-test)

Every user-facing tutorial is a runnable Playwright spec in e2e/tutorials/ (*.tutorial.ts, driven by the helper in e2e/tutorials/tutorial.ts), so a tutorial that no longer matches the app fails, instead of quietly rotting as prose:

npm run tutorial:check    # every published help article maps to a spec or a waiver (see below)
npm run tutorial:record   # run the specs via playwright.tutorial.config.ts → video + step marks
npm run tutorial:docs     # generate per-step markdown walkthroughs into docs/tutorials/
npm run tutorial:video    # compose the narrated video from recordings + narration audio

Coverage is tracked in e2e/tutorials/manifest.json: one entry per help article, naming either the spec that records it or a waiver saying why there is nothing to record. tutorial:check enumerates the articles from app/api/admin/help/seed/route.ts — the /help pages render from MongoDB, so the seed is the repo's source of truth, and seeding is additive, so production can serve articles the seed no longer names (the check reports those rather than failing on them). Add -- --strict to fail on "status": "todo" entries; that is the gate for "ready to record".

Auth-gated tutorials skip (never fail) unless TUTORIAL_STORAGE_STATE points at a signed-in Playwright storage state (e.g. .auth/tutorial-user.json). The generated walkthroughs are committed at docs/tutorials/; recordings, step marks, narration audio, storage states, and composed video are gitignored (tutorial-output/, audio/, .auth/, docs/tutorials/video/). The per-step narration master lives in the witus repo at plans/31-tutorial-narration-scripts.md.

Curated content seeds

Two seed scripts load repo-authored content into MongoDB. Both are idempotent, so re-running after an edit updates rows in place instead of duplicating them.

npm run seed:standards                                   # curriculum standards from lib/data/standards/
npm run seed:math -- [email protected] --dry-run
npm run seed:math -- [email protected]

seed:math creates the math library: 134 sets, 1,838 cards. Every set holds 10 to 20 cards.

Area Sets Cards
Addition, per number 22 242
Subtraction, by what you take away 11 121
Subtraction, by what you take from 9 121
Multiplication, per number 22 242
Division, per divisor 10 110
Patterns (doubles, ways to make 10, squares) 3 33
Mixed review, all four operations 12 180
Geometry 14 256
Trigonometry 14 256
Calculus 17 277
  • Math fact sets hold 11 cards, one focus number crossed with 0 through 10. Addition and multiplication get two sets per number, one for each position of that number in the problem (1 + 0 to 1 + 10 and 0 + 1 to 10 + 1), so recall is drilled in both directions. Subtraction and division are the exact inverses, so every answer is a whole number from 0 to 10.
  • Fact cards are generated by lib/data/math-facts.ts and carry a question and an answer only. They used to ship authored multiple-choice options, which were removed because fact fluency is recall: picking 49 out of four numbers is an easier task than producing 49, and a card cleared that way would still tell the scheduler it is mastered. The study setup screen turns Multiple Choice off for any set tagged math-facts.
  • Reference content is authored JSON in lib/data/math-reference/. Edit it without touching TypeScript; lib/data/math-reference/loadSets.ts validates set size, duplicate questions, and characters the card renderer would read as markup.
  • --owner-email is required and the account must already exist. The script will not guess which account owns public sets. --dry-run reports changes without writing, --only=<slug prefix> seeds a slice, --private keeps the sets unlisted, and --feature marks them featured on Explore (skip it unless you want all 65 fact sets pinned).

Cards are matched on a stable externalId, so a re-seed preserves card _ids and every student's review history survives a content edit.

Pricing

Plan Price
Free $0 (limited AI generations)
Monthly Pro $10.60/month
Lifetime Learner $103.29 one-time (first 100 users)

API Tiers

Two key types share the tier table. Choose based on your use case:

  • Public (fl_pub_). For apps building on top of FlashLearnAI (study apps, LMS integrations).
  • Ecosystem (fl_eco_). For cross-product partners using FlashLearnAI as their backend. Two paths: the child/curriculum flow (learner-scoped sessions, mastery, cascade-delete, signed webhooks) and the standard Sets + Study API for authored decks with per-student progress via externalStudentId. Admin-issued.
Tier Price Generations/mo API calls/mo Burst/min
Free $0 100 (public) / 1,000 (ecosystem) 1,000 / 10,000 10 / 60
Developer $19/mo 5,000 / 10,000 50,000 / 100,000 60 / 120
Pro $49/mo 25,000 / 50,000 250,000 / 500,000 120 / 300
Enterprise Custom Unlimited Unlimited 300 / 600

White-Label App

License Price
Standard $499 one-time (1 domain)
School & Enterprise $999/year (unlimited domains, priority support)

Documentation

  • API Getting Started
  • Interactive API Reference. All 30 paths and 34 operations in lib/api/openapi.ts, plus the session.completed webhook. Session-authenticated app routes, including the /api/teacher roster, claim, and student progress routes, are not part of the v1 surface and are not in the spec.
  • Ecosystem API (cross-product partners)
  • Webhooks. Signing, retry, replay.
  • Roadmap
  • Changelog
  • Help Center. Articles live in the database, not in the repo, and are published by the seed button on /admin/settings. A fresh environment shows an empty help centre until that runs. Readers answer "Was this article helpful?" at the foot of each article whether or not they are signed in; counts appear on /admin/help and any comment goes to the Inbox with the rest of the feedback.

License

Proprietary. All rights reserved.

White-Label Starter App (standalone/flashlearn-starter/) is sold under a commercial license. See white-label pricing.


A WitUS.Online product by B4C LLC.

About

AI-powered flashcard creation and multiplayer study challenges. Transform any content into AI-generated flashcards. Our spaced repetition algorithm ensures you remember what you learn, saving you hours of study time.

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages