Flashcards that stick: build decks, study them with flip cards, and test yourself with multiple-choice quizzes generated from your own material.
Memorising terms, definitions or dates works best with active recall, but spreadsheets and paper cards make it slow to build material and impossible to test yourself properly. QuizDeck turns a list of term and definition pairs into two study modes: flip cards with self-grading, and a scored multiple-choice test whose wrong answers come from the deck itself, so the options are always plausible. Decks can stay private or be shared publicly for anyone to study or copy.
Live demo: not deployed. The repository contains a production-ready deployment setup (
docs/deployment.md), but no public instance is running.
- Decks: create, edit, reorder cards by drag-and-drop or keyboard, pin favourites, keep private or publish; unsaved-changes guard.
- Study mode: 3D flip cards, keyboard shortcuts, shuffle, "got it" / "still learning" grading, review only what you missed.
- Test mode: auto-generated multiple-choice questions, back and forward navigation, warning before submitting with gaps, scored review, retry missed.
- Explore: public catalogue with search, sort and pagination, usable without an account; copy any public deck into your library.
- Accounts: sign up, e-mail verification, password reset, change password, log out everywhere, delete account.
- Quality: responsive and keyboard accessible (axe-checked), server-side rendered with real 404s, SEO metadata, sitemap.
| Library | Study | Test |
|---|---|---|
![]() |
![]() |
![]() |
| Deck | Editor | Mobile |
|---|---|---|
![]() |
![]() |
![]() |
| Area | Choice | Why |
|---|---|---|
| Web | Nuxt 3 (Vue 3, TypeScript), Pinia, Tailwind CSS | SSR for fast first paint, real status codes and indexable public decks; kept from the original project |
| API | NestJS 11 (TypeScript), REST + OpenAPI | Modules, DI and guards give clear boundaries in a modular monolith |
| Database | PostgreSQL 17, TypeORM 0.3 with versioned migrations | Relational integrity (FKs, unique and check constraints) plus jsonb for ordered cards |
| Auth | HttpOnly cookie sessions: 15-min JWT + rotating refresh tokens, Argon2id | XSS cannot read the session; sessions are revocable (ADR 0002) |
| Edge | Caddy 2 | Automatic HTTPS and renewal, one origin for web and API |
| Ops | Docker, Compose, GitHub Actions, GHCR | Reproducible builds, one-command local stack, gated releases |
| Observability | pino JSON logs, Prometheus metrics, Terminus health checks, optional Sentry | Diagnosable without a heavy monitoring stack |
| Tests | Jest, Supertest, Vitest, @nuxt/test-utils, Playwright, axe, k6 | Unit to end-to-end coverage of the critical paths |
Deliberately not used: microservices, Kubernetes, Redis, message queues.
At this scale they add operational cost without benefit (ADR 0001,
docs/performance.md lists when that would change).
flowchart LR
Browser -- HTTPS --> Caddy
Caddy -- "/api/*" --> API["NestJS API<br/>auth · users · decks"]
Caddy -- "pages" --> Web["Nuxt SSR"]
Web -- "SSR fetch (visitor cookies)" --> API
API --> DB[(PostgreSQL)]
API --> SMTP[SMTP]
More: architecture · ER diagram · deployment · auth sequences · ADRs.
Requirements: Docker with Compose v2. Node.js 24 only for running tests or developing outside containers.
git clone https://github.com/oiibar/QuizDeck.git
cd QuizDeck
cp .env.example .env
# set JWT_SECRET in .env, e.g.:
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
docker compose up -d --build --wait # db, mailpit, api, web
docker compose run --rm migrate # create the schema
docker compose run --rm seed # optional demo data| URL | What |
|---|---|
| http://localhost:3000 | Web app |
| http://localhost:4000/api/docs | Swagger UI (OpenAPI) |
| http://localhost:8025 | Mailpit: verification and reset e-mails land here |
Demo login after seeding: demo@quizdeck.dev / Demo-password-1.
Stop with docker compose down (add -v to also delete the database volume).
docker compose up -d db mailpit
cd server && cp .env.example .env && npm ci # set JWT_SECRET in server/.env
npm run migration:run && npm run seed && npm run start:dev
cd ../client && npm ci && npm run dev # proxies /api to localhost:4000Compose reads the root .env.example. The API's full list,
with defaults, is in server/.env.example and is
validated at startup: the API refuses to boot and names the problem if a
value is missing or invalid. The web client's variables are in
client/.env.example.
| Variable | Required | Purpose |
|---|---|---|
JWT_SECRET |
yes | Signs access tokens; 32+ characters |
DATABASE_URL |
yes (set by Compose) | PostgreSQL connection |
APP_URL, CORS_ORIGINS |
defaults to localhost | Public origin for links, CSRF and CORS checks |
MAIL_TRANSPORT, SMTP_*, MAIL_FROM |
for e-mail | smtp (Mailpit locally) or log |
COOKIE_SECURE, TRUST_PROXY |
production | Secure cookies; trust Caddy's forwarded IP |
SENTRY_DSN |
no | Error tracking |
Migrations live in server/src/database/migrations and run as an explicit
step (docker compose run --rm migrate). The seed is idempotent and uses
fictional accounts only. Backup and restore:
./scripts/backup-db.sh
./scripts/restore-db.sh backups/<file>.dump # rehearse into a scratch DB
./scripts/restore-db.sh backups/<file>.dump --replace # real restoreDetails: docs/database.md, docs/disaster-recovery.md.
# server/ (API tests need the Compose db and a quizdeck_test database, see docs/testing.md)
npm run lint && npm run typecheck && npm test && npm run test:e2e
# client/
npm run lint && npm run typecheck && npm test
npm run test:e2e # Playwright against the running stackLatest local run (2026-11-10): 23 server unit, 48 API/integration, 37 client unit and component, and 12 Playwright tests, all passing. Combined server coverage is 95% of lines and 67% of branches. Under a 50-user k6 load test the API served 164 requests per second with a p95 of 48 ms and no errors, on a laptop with seed data. Conditions and query-plan analysis are in docs/performance.md.
CI (GitHub Actions) runs format, lint, typecheck, all test suites, builds,
npm audit, Trivy image scans, a Gitleaks secret scan and Playwright against
the Compose stack on every push and pull request.
One Linux VM with Docker Compose and Caddy (automatic Let's Encrypt). Tagging
vX.Y.Z builds and publishes images to GHCR. After approval on the
production environment, CI deploys over SSH: backup, migrate, roll out,
smoke test. Rollback is redeploying the previous tag. Full guide:
docs/deployment.md. Operations:
docs/operations-runbook.md.
Argon2id passwords; HttpOnly SameSite cookies with rotating refresh tokens and reuse detection; ownership checks on every deck operation (private decks of others return 404); strict input whitelisting; per-IP rate limits with a stricter limit on credential endpoints; CSRF origin checks; Helmet and CSP headers; secrets only from the environment; credentials and tokens redacted from logs. See docs/security.md and SECURITY.md.
The pre-1.0 code committed database credentials. They were removed but remain in Git history and must be rotated at the provider.
- Not deployed publicly yet; production backups and monitoring are documented but not exercised on a real host.
- Web CSP allows inline scripts (Nuxt hydration); nonces are the next step.
- Rate limits are per process; a multi-replica setup needs Redis-backed throttling.
- No spaced-repetition scheduling, images or audio on cards, tags or folders, social login or MFA.
- Search uses
ILIKE; apg_trgmindex is needed for a large catalogue. - No licence has been chosen yet, so all rights are reserved by the author.
Product requirements · Architecture · API · Database · Security · Testing · Performance · Deployment · Runbook · Disaster recovery · Audit of the original code · Contributing · Changelog
Built by oiibar. Contact: contact-me-neon.vercel.app.






