This is a full-featured flashcard learning application built for Val Town, implementing spaced repetition learning (SM-2 algorithm) with comprehensive statistics tracking and backup/restore capabilities.
- Backend Framework: Hono 4.0.2
- Frontend: Vanilla JavaScript with TailwindCSS
- Storage: Val Town Blob Storage (per-user isolation)
- Runtime: Deno on Val Town serverless platform
- Algorithm: Simplified SM-2 Spaced Repetition (Anki-style)
flashcard/
βββ main.ts # Entry point (exports backend/index.ts)
βββ backend/
β βββ index.ts # Hono app setup and route mounting
β βββ storage.ts # Blob storage utilities
β βββ routes/
β βββ auth.ts # User authentication
β βββ decks.ts # Deck CRUD operations
β βββ cards.ts # Card CRUD operations
β βββ study.ts # Spaced repetition study mode
β βββ stats.ts # Statistics and session tracking
β βββ backup.ts # Data backup/restore
βββ frontend/
β βββ index.html # Single-page application UI
β βββ index.js # Client-side logic and API calls
βββ shared/
βββ types.ts # Shared TypeScript types and SR algorithm
Stored in blob storage with key: flashcard_user_{username}
{
username: string,
decks: Deck[],
cards: Card[],
sessions: StudySession[],
stats: UserStats
}
{
id: string, // Generated unique ID
name: string, // Deck name
description: string, // Optional description
createdAt: string // ISO date string
}
{
id: string, // Generated unique ID
deckId: string, // Parent deck reference
front: string, // Question/prompt
back: string, // Answer
easeFactor: number, // SM-2 ease factor (default: 2.5)
interval: number, // Days until next review
nextReview: string, // ISO date string
reviewCount: number // Total times reviewed
}
{
id: string, // Generated unique ID
date: string, // ISO date string
deckId: string, // Deck studied
deckName: string, // Deck name (denormalized)
duration: number, // Session duration in seconds
cardsReviewed: number, // Total cards reviewed
correctCount: number // Cards rated "Good" or "Easy"
}
{
totalCards: number, // Total cards created
totalReviews: number, // Total reviews across all sessions
totalTime: number, // Total study time in seconds
averageAccuracy: number // Percentage (0-100)
}
The app uses a simplified version of the SuperMemo SM-2 algorithm, which is the foundation of Anki's spaced repetition system.
- easeFactor: 2.5
- interval: 0 (new cards are due immediately)
- nextReview: Current date/time
Users rate their recall on a 4-point scale:
-
Again (Failed): Didn't remember the answer
- Reset interval to 1 day
- Reduce easeFactor by 0.2 (min: 1.3)
-
Hard (Difficult): Remembered with difficulty
- Multiply interval by 1.2
- Reduce easeFactor by 0.15 (min: 1.3)
-
Good (Normal): Remembered correctly with some effort
- First review: 1 day
- Second review: 6 days
- Subsequent: interval Γ easeFactor
-
Easy (Perfect): Remembered instantly
- First review: 4 days
- Subsequent: interval Γ easeFactor Γ 1.3
- Increase easeFactor by 0.15
Cards are considered "due" when:
new Date(card.nextReview) <= new Date()
Cards are presented in order of their nextReview date (earliest first).
- Optimizes retention: Reviews cards just before you're likely to forget them
- Reduces study time: Focuses on difficult cards, spaces out easy ones
- Long-term retention: Proven effective for language learning, medical studies, etc.
POST /api/auth/login
- Body:
{ username: string } - Returns: Full
UserDataobject - Creates new user if doesn't exist
GET /api/decks
- Headers:
X-Username: string - Returns: Array of
Deckobjects
POST /api/decks
- Headers:
X-Username: string - Body:
{ name: string, description?: string } - Returns: Created
Deck
PUT /api/decks/:id
- Headers:
X-Username: string - Body:
{ name?: string, description?: string } - Returns: Updated
Deck
DELETE /api/decks/:id
- Headers:
X-Username: string - Returns:
{ success: true } - Cascades: Deletes all cards and sessions for the deck
GET /api/cards/deck/:deckId
- Headers:
X-Username: string - Returns: Array of
Cardobjects
POST /api/cards
- Headers:
X-Username: string - Body:
{ deckId: string, front: string, back: string } - Returns: Created
Card
PUT /api/cards/:id
- Headers:
X-Username: string - Body:
{ front?: string, back?: string } - Returns: Updated
Card
DELETE /api/cards/:id
- Headers:
X-Username: string - Returns:
{ success: true }
GET /api/study/:deckId
- Headers:
X-Username: string - Returns: Array of due
Cardobjects (sorted by nextReview)
POST /api/study/review
- Headers:
X-Username: string - Body:
{ cardId: string, rating: number (1-4) } - Returns:
{ card: Card, reviewResult: ReviewResult } - Updates card's SM-2 parameters
GET /api/stats
- Headers:
X-Username: string - Returns:
{ stats: UserStats, sessions: StudySession[] }
POST /api/stats/sessions
- Headers:
X-Username: string - Body:
{ deckId: string, duration: number, cardsReviewed: number, correctCount: number } - Returns: Created
StudySession
GET /api/backup
- Headers:
X-Username: string - Returns: Full
UserDataJSON - Sets Content-Disposition header for download
POST /api/backup/restore
- Headers:
X-Username: string - Body: Full
UserDataJSON - Returns:
{ success: true, message: string } - Replaces all user data
The app uses a view-based navigation system where only one view is active at a time:
- Login View: Username entry
- Deck List View: Dashboard of all decks
- Deck Editor View: Create/edit deck
- Card Manager View: List and edit cards in a deck
- Study View: Spaced repetition study session
- Statistics View: Performance metrics and session history
- Backup View: Export/import data
Global state variables:
currentUser: Logged-in usernameuserData: Full user data objectcurrentDeckId: Active deckstudyCards: Cards in current study sessionsessionStartTime: Study session start timestamp
Session persistence:
- Username stored in
sessionStorage - Auto-login on page reload
All API calls:
- Use
apiCall()wrapper function - Include
X-Usernameheader for authenticated requests - Handle errors with user-friendly messages
- Return parsed JSON responses
- Load due cards from
/api/study/:deckId - Start timer and initialize counters
- For each card:
- Show front (question)
- User flips card to see back (answer)
- User rates recall (Again/Hard/Good/Easy)
- Update card via
/api/study/review - Track correct answers (Good/Easy)
- On completion:
- Calculate duration and accuracy
- Save session via
/api/stats/sessions - Show results summary
Card Flip Animation
- CSS 3D transform with
perspectiveandrotateY - Front and back faces with
backface-visibility: hidden - Smooth 0.6s transition
Responsive Design
- TailwindCSS utility classes
- Grid layouts for deck cards
- Mobile-friendly navigation
Each user's data is stored in a separate blob:
- Key pattern:
flashcard_user_{username} - No user can access another user's data
- Simple username-based authentication (suitable for personal/demo use)
Read: blob.getJSON(key) - Loads entire user object
Write: blob.setJSON(key, data) - Saves entire user object
All mutations happen in-memory, then saved atomically.
Users can export their entire dataset as JSON and restore it later, enabling:
- Data portability
- Version control (manual)
- Migration between Val Town accounts
- Recovery from mistakes
Decision: Use Blob Storage
Rationale:
- Simpler per-user isolation
- No schema migrations needed
- Faster reads (single blob fetch vs multiple queries)
- Suitable for personal flashcard app scale
Tradeoffs:
- No cross-user queries (not needed)
- Must load entire user dataset
- No incremental updates (acceptable for this scale)
Decision: No password, just username
Rationale:
- Simplifies UX for personal/demo use
- Val Town already provides account-level security
- Focus on learning features, not auth complexity
Tradeoffs:
- Not suitable for production multi-user app
- Users should use unique usernames
- Sufficient for personal use or trusted environments
Decision: Vanilla JavaScript
Rationale:
- Lighter bundle size
- Simpler debugging
- No build step required
- Easier to understand for learners
Tradeoffs:
- More manual DOM manipulation
- No component reusability
- State management is manual
Decision: Client-side view switching
Rationale:
- Single-page feel
- No page reloads
- Better UX for study sessions
- Simpler state management
Tradeoffs:
- All views loaded upfront
- SEO not a concern for this app type
Decision: Simplified SM-2 with 4 ratings
Rationale:
- Proven algorithm (used by Anki)
- Simple to implement
- Effective for most use cases
- Users understand 4-point scale
Tradeoffs:
- Not as sophisticated as SM-17+
- No auto-tuning of parameters
- Good enough for personal learning
Decision: Store deckName in StudySession
Rationale:
- Preserve history if deck is renamed/deleted
- Faster stats display (no joins)
- Simpler queries
Tradeoffs:
- Data duplication
- Potential inconsistency (acceptable for historical data)
- Single Blob Fetch: Load entire user data once on login
- In-Memory Mutations: Update local state, then save
- Lazy Loading: Only load cards when viewing/studying a deck
- Session Storage: Remember username between page loads
- Batch Updates: Save session data once at completion
Suitable for:
- Up to ~1000 decks per user
- Up to ~10,000 cards per user
- Up to ~1000 sessions per user
Beyond this, consider:
- Pagination
- Incremental loading
- SQLite for relational queries
- Separate blob per deck
- Implement Anki's FSRS algorithm
- Add learning phase (new cards)
- Configurable ease factor bounds
- Review history analysis
- Deck sharing (export/import specific decks)
- Image support in cards (Val Town AI image generation)
- Audio pronunciation
- Cloze deletions
- Card tagging/filtering
- Search functionality
- Deck templates (languages, medicine, etc.)
- Heatmap calendar of study activity
- Review forecast
- Keyboard shortcuts
- Dark mode
- Progressive Web App (offline support)
- Mobile app wrapper
- Undo card deletion
- Bulk card import (CSV)
- Retention graphs
- Card difficulty distribution
- Study time by deck
- Streak tracking
- Learning velocity
- Data Model: Update types in
shared/types.ts - Storage: Update
backend/storage.tsif needed - API: Add routes in
backend/routes/ - UI: Add view in
frontend/index.html - Logic: Implement in
frontend/index.js - Test: Manual testing in Val Town preview
- Backend: Let errors bubble (Hono's
onErrorconfigured) - Frontend: User-friendly alert messages
- API errors: Always include descriptive error messages
- Keep
shared/compatible with both browser and Deno - Use TypeScript types consistently
- Comment complex algorithms
- Maintain separation of concerns (storage/routes/UI)
- Upload project files to Val Town
- Set main entry point to
main.ts - Configure as HTTP Val
- No environment variables needed (blob storage is built-in)
- Local: Run with Deno (requires Val Town SDK)
- Preview: Use Val Town's preview feature
- Production: Deploy to Val Town domain
This flashcard app demonstrates a complete full-stack application built for Val Town's serverless platform, implementing a proven learning algorithm with a clean, functional UI. The architecture prioritizes simplicity and usability while remaining extensible for future enhancements.
The use of spaced repetition makes it genuinely useful for learning, while the statistics tracking provides motivation and insight into learning progress. The backup/restore functionality ensures users never lose their hard work.