Skip to content

Repository files navigation

Pixel-mint — Visual Flashcards That Stick

Spaced repetition for things your eyes have to learn.

CI

Status: beta. Working today: per-visitor workspaces (anonymous sessions), deck/card creation, CSV deck import, image upload with server-side variants, SM-2 scheduling, offline review with idempotent sync, zoom/pan, notes with revision history, stats. Deploys to Vercel as a static frontend + one serverless function. In progress: real accounts (cross-device sync), deck sharing.


Motivation

I tried to memorize a hundred plant species for a botany class with Anki and it was miserable — typing the Latin name on a phone, fighting with image attachments, no good way to study "what does this leaf look like" the way you'd actually be tested. Anki is text-first; my problem was image-first.

Pixel-mint is the smallest flashcard app I could build where the prompt is a picture and the answer is whatever you want it to be. It happens to work for botany, anatomy diagrams, kanji shapes, art history, dog breeds, fungal ID, and anything else where the signal lives in the pixels.

What it does

  • Per-visitor workspace, zero sign-up. A signed anonymous session cookie gives every browser its own decks; new visitors start with a private copy of the starter deck.
  • Decks of cards, each card is image → answer. Create them in the app, or import a CSV (image,answer[,tags]) together with the image files — the same parser runs in the browser and in tests.
  • SM-2 spaced repetition scheduler, runs entirely on-device. Reviews queue up offline and sync when you're back online — idempotently, so retries and races can never double-count a review.
  • Direct-to-bucket image upload via presigned URLs; the server validates by magic bytes (no SVG, ever) and resizes into thumb / review / zoom WebP variants. Clients only ever see short-lived signed URLs; the bucket stays private.
  • Zoom and pan on the image — small details matter, and phones are small.
  • Per-card notes with revision history — every edit is a revision you can walk back.
  • "Minted" view: a daily collage of the cards you got right, because positive reinforcement works.
  • Capacitor wrapper for native iOS/Android builds from the same codebase.

Architecture

                Vercel
   ┌────────────────────────────────┐
   │  static Vite build (CDN)       │
   │  /api/* ► Express, bundled     │──► hosted Postgres (Neon / Supabase, pooled)
   │  as ONE serverless function    │
   └────────────────────────────────┘──► S3-compatible bucket (private)
        ▲            ▲                      ▲         │
        │            │ signed GET URLs      │         │ signed PUT (browser → bucket)
        │            └──────────────────────┼─────────┘
   IndexedDB (offline queue, due cards)     │
        ▲                                   │
   React + Capacitor client ────────────────┘
  • Identity comes from a signed httpOnly cookie; the API derives req.userId from it and never trusts a client-supplied id. Swapping in a real auth provider later only replaces the provisioning step.
  • IndexedDB is the offline cache; queued reviews carry client-generated UUIDs so the server can drop replays (at-least-once delivery, exactly-once effect).
  • @pixelmint/core is a tiny dependency-free package holding the SM-2 algorithm and CSV import logic. The same scheduler runs on-device (offline) and on the server (sync reconciliation), so the two can never drift.
  • Postgres holds users, decks/cards, the append-only review log (which survives card deletion for honest analytics), and versioned notes.
  • The bucket stores only sharp-derived WebP variants — originals are validated, resized, and discarded.

Repository layout

pixel-mint/
├── api/              # the Vercel serverless function (re-exports the server bundle)
├── packages/
│   ├── core/         # framework-free TS: SM-2 scheduler, CSV import, shared types
│   ├── server/       # Express + pg API: CRUD, reviews, stats, image ingest, sessions
│   └── app/          # React + Vite + Capacitor client (offline-first, IndexedDB)
├── db/
│   ├── migrations/   # plain-SQL schema migrations (forward-only runner)
│   ├── seeds/        # dev seed + constraint-violation examples
│   └── queries/      # the gnarly analytical SQL (window functions, recursive CTEs)
├── scripts/          # synthetic data generator (Python + Faker) for load testing
├── infra/            # local docker-compose (Postgres + MinIO); legacy AWS Terraform
├── docs/             # architecture, data model, deployment (Vercel)
└── vercel.json       # build + rewrite config for the Vercel deployment

Quickstart (local, full stack)

You need Docker and Node 20+ (Python 3.10+ only for the synthetic-data script).

# 1. bring up Postgres + MinIO (an S3-compatible store) locally
docker compose -f infra/docker-compose.yml up -d

# 2. install workspace dependencies
npm install

# 3. apply migrations, load the dev seed, create the starter deck (generates images!)
npm run db:migrate
npm run db:seed
npm run db:seed-starter

# 4. run the API (talks to local Postgres + MinIO)
npm run dev:server

# 5. in another terminal, run the web client (proxies /api to the server)
npm run dev:app

Open the printed Vite URL. You'll get your own workspace with the "Leaf shapes 101" starter deck ready to review — images included (they're generated and uploaded to MinIO by db:seed-starter). See docs/deployment.md for the Vercel production deployment and docs/data-model.md for the schema.

Stack

react · vite · typescript · capacitor · express · postgres · zod · s3 (presigned, S3/R2/MinIO) · sharp · vitest · vercel

Development

npm run typecheck   # tsc across core, server, app
npm test            # vitest: SM-2 + CSV (core), schemas/sessions/sniffing (server)
npm run build       # bundle the server (tsup) and the web app (vite)

# integration suite (real Postgres; CI runs this via a service container)
TEST_DATABASE_URL=postgres://pixelmint:pixelmint@localhost:5432/pixelmint_test npm test

# end-to-end smoke against any running deployment (local or Vercel)
./scripts/smoke.sh http://localhost:8787

The db/ directory has the plain-SQL migrations, the hand-written dev seed (with a companion file of rows that should be rejected, to prove the constraints), and the analytical queries. scripts/gen_synthetic.py generates a large dataset for load-testing them.

Roadmap

  • Deck / card CRUD (now with in-app creation UI)
  • SM-2 scheduler (shared core, on-device + server)
  • Offline review queue (IndexedDB) with idempotent sync
  • Zoom / pan review screen
  • Direct-to-bucket image upload → server-side WebP variants
  • Per-card notes with revision history
  • Per-visitor anonymous workspaces
  • CSV deck import (browser-side, with image matching)
  • Vercel deployment (static + serverless)
  • Real accounts (cross-device sync via an auth provider)
  • Deck sharing / public decks
  • Native push reminders ("you have 12 cards due")

Why "pixelmint"

Pixels in, freshly-minted memory out. That's the whole joke.

License

MIT — see LICENSE.