Deploying a Digiflashcards App
Introduction
Digiflashcards is a powerful digital flashcard platform that transforms how students, educators, and lifelong learners master new information through scientifically-proven spaced repetition techniques. Built for modern learning, Digiflashcards combines the proven effectiveness of traditional flashcards with advanced algorithms, multimedia support, and collaborative features to maximize retention and accelerate learning.
Digiflashcards stands out with its:
- Spaced Repetition Algorithm: Intelligent SM-2 algorithm optimizes review intervals for maximum retention
- Multimedia Flashcards: Support for images, audio, video, LaTeX equations, and code snippets
- Rich Text Editor: Create beautifully formatted cards with markdown, syntax highlighting, and diagrams
- Deck Management: Organize flashcards into decks and sub-decks with tags and categories
- Progress Tracking: Detailed statistics on learning progress, accuracy rates, and time spent studying
- Study Modes: Multiple study modes including classic flashcards, multiple choice, typing answers, and cloze deletion
- Collaborative Learning: Share decks with friends, classmates, or the entire community
- Import/Export: Import cards from Anki, Quizlet, CSV, and other formats
- Mobile Responsive: Study anywhere with a fully responsive design that works on all devices
- Offline Support: Progressive Web App (PWA) capabilities for offline studying
- Custom Scheduling: Adjust difficulty levels and review schedules for individual cards
- Learning Streaks: Gamification features to maintain daily study habits and motivation
- AI-Generated Cards: Optional AI assistance to generate flashcards from text content
- API Access: RESTful API for integration with learning management systems and custom apps
- Multi-Language Support: Study in any language with full Unicode support
This comprehensive guide walks you through deploying Digiflashcards on Klutch.sh using Docker, covering installation, database setup, file storage, environment configuration, and production best practices for educational platforms.
Prerequisites
Before you begin deploying Digiflashcards, ensure you have the following:
- A Klutch.sh account
- A GitHub account with a repository for your Digiflashcards project
- Docker installed locally for testing (optional but recommended)
- Basic understanding of Docker, databases, and web applications
- A domain name for your Digiflashcards instance (recommended for production)
Project Structure
Here’s the recommended project structure for a Digiflashcards deployment:
digiflashcards/├── Dockerfile├── package.json├── tsconfig.json├── .dockerignore├── .gitignore├── src/│ ├── index.ts│ ├── config/│ │ ├── database.ts│ │ └── spaced-repetition.ts│ ├── models/│ │ ├── User.ts│ │ ├── Deck.ts│ │ ├── Card.ts│ │ ├── StudySession.ts│ │ └── ReviewLog.ts│ ├── services/│ │ ├── spacedRepetition.service.ts│ │ ├── deck.service.ts│ │ ├── card.service.ts│ │ └── import.service.ts│ ├── routes/│ │ ├── deck.routes.ts│ │ ├── card.routes.ts│ │ ├── study.routes.ts│ │ └── user.routes.ts│ ├── middleware/│ │ ├── auth.middleware.ts│ │ ├── upload.middleware.ts│ │ └── validation.middleware.ts│ └── utils/│ ├── sm2-algorithm.ts│ ├── markdown-parser.ts│ └── logger.ts└── uploads/ ├── images/ └── audio/Step 1: Create the Dockerfile
Create a Dockerfile in the root of your project. This Dockerfile sets up a Node.js environment for the flashcard application:
# Use Node.js 20 Alpine for smaller image sizeFROM node:20-alpine AS builder
# Install build dependenciesRUN apk add --no-cache \ python3 \ make \ g++ \ cairo-dev \ jpeg-dev \ pango-dev \ giflib-dev
# Set working directoryWORKDIR /app
# Copy package filesCOPY package*.json ./COPY tsconfig.json ./
# Install dependenciesRUN npm ci --only=production && \ npm cache clean --force
# Copy application sourceCOPY . .
# Build TypeScript applicationRUN npm run build
# Production stageFROM node:20-alpine
# Install runtime dependenciesRUN apk add --no-cache \ cairo \ jpeg \ pango \ giflib \ ffmpeg
# Create non-root userRUN addgroup -g 1001 -S nodejs && \ adduser -S nodejs -u 1001
# Set working directoryWORKDIR /app
# Copy built application from builderCOPY --from=builder --chown=nodejs:nodejs /app/dist ./distCOPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modulesCOPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
# Create directories for uploadsRUN mkdir -p /app/uploads/images /app/uploads/audio /app/uploads/temp && \ chown -R nodejs:nodejs /app/uploads
# Switch to non-root userUSER nodejs
# Expose application portEXPOSE 3000
# Health checkHEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Start the applicationCMD ["node", "dist/index.js"]Step 2: Create the Application
Create the main application file src/index.ts:
import express, { Express, Request, Response } from 'express';import cors from 'cors';import helmet from 'helmet';import morgan from 'morgan';import { Pool } from 'pg';import dotenv from 'dotenv';import deckRoutes from './routes/deck.routes';import cardRoutes from './routes/card.routes';import studyRoutes from './routes/study.routes';import userRoutes from './routes/user.routes';import { setupDatabase } from './config/database';import { logger } from './utils/logger';
dotenv.config();
const app: Express = express();const PORT = process.env.PORT || 3000;
// Database connectionexport const db = new Pool({ host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT || '5432'), database: process.env.DB_NAME || 'digiflashcards', user: process.env.DB_USER || 'postgres', password: process.env.DB_PASSWORD, max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 10000,});
// Middlewareapp.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], imgSrc: ["'self'", "data:", "blob:", "https:"], mediaSrc: ["'self'", "blob:"], scriptSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"], }, },}));
app.use(cors({ origin: process.env.CORS_ORIGIN || '*', credentials: true,}));
app.use(morgan('combined'));app.use(express.json({ limit: '10mb' }));app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Static file serving for uploadsapp.use('/uploads', express.static('uploads'));
// API Routesapp.use('/api/decks', deckRoutes);app.use('/api/cards', cardRoutes);app.use('/api/study', studyRoutes);app.use('/api/users', userRoutes);
// Health check endpointapp.get('/health', async (req: Request, res: Response) => { try { await db.query('SELECT 1'); res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), database: 'connected', }); } catch (error) { res.status(503).json({ status: 'unhealthy', timestamp: new Date().toISOString(), database: 'disconnected', }); }});
// Root endpointapp.get('/', (req: Request, res: Response) => { res.json({ name: 'Digiflashcards API', version: '1.0.0', description: 'Digital flashcard platform with spaced repetition', endpoints: { health: '/health', decks: '/api/decks', cards: '/api/cards', study: '/api/study', users: '/api/users', }, });});
// 404 handlerapp.use((req: Request, res: Response) => { res.status(404).json({ error: 'Not Found', message: 'The requested resource does not exist', });});
// Error handlerapp.use((err: any, req: Request, res: Response, next: any) => { logger.error('Unhandled error:', err); res.status(err.status || 500).json({ error: 'Internal Server Error', message: process.env.NODE_ENV === 'production' ? 'An error occurred' : err.message, });});
// Initialize applicationasync function startServer() { try { // Setup database await setupDatabase(db); logger.info('Database initialized successfully');
// Start server app.listen(PORT, () => { logger.info(`Digiflashcards server running on port ${PORT}`); logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`); }); } catch (error) { logger.error('Failed to start server:', error); process.exit(1); }}
// Handle graceful shutdownprocess.on('SIGTERM', async () => { logger.info('SIGTERM received, closing server gracefully'); await db.end(); process.exit(0);});
process.on('SIGINT', async () => { logger.info('SIGINT received, closing server gracefully'); await db.end(); process.exit(0);});
startServer();Step 3: Create the Database Configuration
Create src/config/database.ts to set up the database schema:
import { Pool } from 'pg';import { logger } from '../utils/logger';
export async function setupDatabase(db: Pool): Promise<void> { try { await db.query('BEGIN');
// Users table await db.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, full_name VARCHAR(255), avatar_url VARCHAR(500), timezone VARCHAR(100) DEFAULT 'UTC', daily_goal INTEGER DEFAULT 20, notification_enabled BOOLEAN DEFAULT true, theme VARCHAR(20) DEFAULT 'light', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `);
// Decks table await db.query(` CREATE TABLE IF NOT EXISTS decks ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, parent_deck_id INTEGER REFERENCES decks(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, description TEXT, cover_image VARCHAR(500), is_public BOOLEAN DEFAULT false, tags TEXT[], card_count INTEGER DEFAULT 0, study_count INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `);
// Cards table await db.query(` CREATE TABLE IF NOT EXISTS cards ( id SERIAL PRIMARY KEY, deck_id INTEGER REFERENCES decks(id) ON DELETE CASCADE, front_content TEXT NOT NULL, back_content TEXT NOT NULL, front_media VARCHAR(500), back_media VARCHAR(500), card_type VARCHAR(50) DEFAULT 'basic', difficulty INTEGER DEFAULT 0, ease_factor DECIMAL(5,2) DEFAULT 2.5, interval INTEGER DEFAULT 0, repetitions INTEGER DEFAULT 0, next_review_date TIMESTAMP, tags TEXT[], notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `);
// Study sessions table await db.query(` CREATE TABLE IF NOT EXISTS study_sessions ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, deck_id INTEGER REFERENCES decks(id) ON DELETE CASCADE, started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, ended_at TIMESTAMP, cards_studied INTEGER DEFAULT 0, cards_correct INTEGER DEFAULT 0, cards_incorrect INTEGER DEFAULT 0, total_time_seconds INTEGER DEFAULT 0, session_type VARCHAR(50) DEFAULT 'review' ) `);
// Review logs table await db.query(` CREATE TABLE IF NOT EXISTS review_logs ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, card_id INTEGER REFERENCES cards(id) ON DELETE CASCADE, session_id INTEGER REFERENCES study_sessions(id) ON DELETE SET NULL, rating INTEGER NOT NULL, time_taken_seconds INTEGER, previous_ease_factor DECIMAL(5,2), new_ease_factor DECIMAL(5,2), previous_interval INTEGER, new_interval INTEGER, reviewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `);
// Shared decks table await db.query(` CREATE TABLE IF NOT EXISTS shared_decks ( id SERIAL PRIMARY KEY, deck_id INTEGER REFERENCES decks(id) ON DELETE CASCADE, shared_by_user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, share_code VARCHAR(50) UNIQUE NOT NULL, download_count INTEGER DEFAULT 0, is_active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `);
// User statistics table await db.query(` CREATE TABLE IF NOT EXISTS user_statistics ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE UNIQUE, total_cards_studied INTEGER DEFAULT 0, total_study_time_seconds INTEGER DEFAULT 0, current_streak_days INTEGER DEFAULT 0, longest_streak_days INTEGER DEFAULT 0, last_study_date DATE, cards_due_today INTEGER DEFAULT 0, cards_due_tomorrow INTEGER DEFAULT 0, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `);
// Import history table await db.query(` CREATE TABLE IF NOT EXISTS import_history ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, deck_id INTEGER REFERENCES decks(id) ON DELETE CASCADE, import_type VARCHAR(50) NOT NULL, file_name VARCHAR(500), cards_imported INTEGER DEFAULT 0, status VARCHAR(50) DEFAULT 'pending', error_message TEXT, imported_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `);
// Create indexes for better performance await db.query(` CREATE INDEX IF NOT EXISTS idx_decks_user_id ON decks(user_id); CREATE INDEX IF NOT EXISTS idx_decks_parent_deck_id ON decks(parent_deck_id); CREATE INDEX IF NOT EXISTS idx_cards_deck_id ON cards(deck_id); CREATE INDEX IF NOT EXISTS idx_cards_next_review_date ON cards(next_review_date); CREATE INDEX IF NOT EXISTS idx_study_sessions_user_id ON study_sessions(user_id); CREATE INDEX IF NOT EXISTS idx_study_sessions_deck_id ON study_sessions(deck_id); CREATE INDEX IF NOT EXISTS idx_review_logs_user_id ON review_logs(user_id); CREATE INDEX IF NOT EXISTS idx_review_logs_card_id ON review_logs(card_id); CREATE INDEX IF NOT EXISTS idx_shared_decks_share_code ON shared_decks(share_code); `);
// Create trigger for updating updated_at timestamp await db.query(` CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ language 'plpgsql'; `);
await db.query(` DROP TRIGGER IF EXISTS update_users_updated_at ON users; CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); `);
await db.query(` DROP TRIGGER IF EXISTS update_decks_updated_at ON decks; CREATE TRIGGER update_decks_updated_at BEFORE UPDATE ON decks FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); `);
await db.query(` DROP TRIGGER IF EXISTS update_cards_updated_at ON cards; CREATE TRIGGER update_cards_updated_at BEFORE UPDATE ON cards FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); `);
await db.query('COMMIT'); logger.info('Database schema created successfully'); } catch (error) { await db.query('ROLLBACK'); logger.error('Database setup failed:', error); throw error; }}Step 4: Create Spaced Repetition Algorithm
Create src/utils/sm2-algorithm.ts for the SM-2 spaced repetition algorithm:
export interface CardReview { cardId: number; rating: number; // 0-5 scale previousEaseFactor: number; previousInterval: number; previousRepetitions: number;}
export interface ReviewResult { easeFactor: number; interval: number; repetitions: number; nextReviewDate: Date;}
/** * SM-2 Algorithm for Spaced Repetition * Ratings: * 0 - Complete blackout * 1 - Incorrect response, but correct answer seemed familiar * 2 - Incorrect response, correct answer easy to recall * 3 - Correct response with serious difficulty * 4 - Correct response after hesitation * 5 - Perfect response */export function calculateNextReview(review: CardReview): ReviewResult { let { rating, previousEaseFactor, previousInterval, previousRepetitions } = review;
// Ensure rating is within bounds rating = Math.max(0, Math.min(5, rating));
let easeFactor = previousEaseFactor; let interval = previousInterval; let repetitions = previousRepetitions;
// Calculate new ease factor easeFactor = Math.max( 1.3, easeFactor + (0.1 - (5 - rating) * (0.08 + (5 - rating) * 0.02)) );
// If rating < 3, reset the card if (rating < 3) { repetitions = 0; interval = 1; } else { repetitions += 1;
if (repetitions === 1) { interval = 1; } else if (repetitions === 2) { interval = 6; } else { interval = Math.round(interval * easeFactor); } }
// Calculate next review date const nextReviewDate = new Date(); nextReviewDate.setDate(nextReviewDate.getDate() + interval);
return { easeFactor: Math.round(easeFactor * 100) / 100, interval, repetitions, nextReviewDate, };}
export function getDueCards(cards: any[]): any[] { const now = new Date(); return cards.filter(card => { if (!card.next_review_date) return true; return new Date(card.next_review_date) <= now; });}
export function getCardDifficulty(easeFactor: number): string { if (easeFactor >= 2.5) return 'easy'; if (easeFactor >= 2.0) return 'medium'; if (easeFactor >= 1.5) return 'hard'; return 'very-hard';}Step 5: Create Package Configuration
Create a package.json file:
{ "name": "digiflashcards", "version": "1.0.0", "description": "Digital flashcard platform with spaced repetition", "main": "dist/index.js", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "ts-node src/index.ts", "test": "jest" }, "dependencies": { "express": "^4.18.2", "cors": "^2.8.5", "helmet": "^7.1.0", "morgan": "^1.10.0", "pg": "^8.11.3", "dotenv": "^16.3.1", "multer": "^1.4.5-lts.1", "bcryptjs": "^2.4.3", "jsonwebtoken": "^9.0.2", "marked": "^11.1.1", "dompurify": "^3.0.8", "jsdom": "^23.2.0", "sharp": "^0.33.0", "csv-parse": "^5.5.3", "uuid": "^9.0.1", "date-fns": "^3.0.6", "joi": "^17.11.0" }, "devDependencies": { "@types/express": "^4.17.21", "@types/cors": "^2.8.17", "@types/morgan": "^1.9.9", "@types/pg": "^8.10.9", "@types/multer": "^1.4.11", "@types/bcryptjs": "^2.4.6", "@types/jsonwebtoken": "^9.0.5", "@types/uuid": "^9.0.7", "@types/node": "^20.10.6", "@types/marked": "^6.0.0", "@types/dompurify": "^3.0.5", "@types/jsdom": "^21.1.6", "typescript": "^5.3.3", "ts-node": "^10.9.2" }, "engines": { "node": ">=20.0.0", "npm": ">=10.0.0" }}Step 6: Create TypeScript Configuration
Create a tsconfig.json file:
{ "compilerOptions": { "target": "ES2022", "module": "commonjs", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "moduleResolution": "node", "declaration": true, "declarationMap": true, "sourceMap": true, "removeComments": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts"]}Step 7: Create Environment Configuration
Create a .env.example file with the necessary environment variables:
# Server ConfigurationNODE_ENV=productionPORT=3000
# Database ConfigurationDB_HOST=your-database-hostDB_PORT=5432DB_NAME=digiflashcardsDB_USER=postgresDB_PASSWORD=your-secure-password
# Authentication ConfigurationJWT_SECRET=your-jwt-secret-key-change-thisJWT_EXPIRES_IN=7d
# Upload ConfigurationUPLOAD_DIR=/app/uploadsMAX_UPLOAD_SIZE=5242880ALLOWED_IMAGE_TYPES=image/jpeg,image/png,image/gif,image/webpALLOWED_AUDIO_TYPES=audio/mpeg,audio/wav,audio/ogg
# CORS ConfigurationCORS_ORIGIN=*
# Spaced Repetition ConfigurationDEFAULT_EASE_FACTOR=2.5MIN_EASE_FACTOR=1.3MAX_INTERVAL_DAYS=365NEW_CARDS_PER_DAY=20
# Email Configuration (optional)SMTP_HOST=SMTP_PORT=587SMTP_USER=SMTP_PASS=SMTP_FROM=noreply@digiflashcards.com
# Feature FlagsENABLE_PUBLIC_DECKS=trueENABLE_DECK_SHARING=trueENABLE_IMPORT_EXPORT=trueENABLE_AI_GENERATION=false
# Analytics ConfigurationANALYTICS_ENABLED=falseANALYTICS_PROVIDER=
# Logging ConfigurationLOG_LEVEL=infoLOG_TO_FILE=trueLOG_DIR=/app/logsStep 8: Push to GitHub
Initialize a Git repository and push your code:
git initgit add .git commit -m "Initial Digiflashcards deployment setup"git branch -M maingit remote add origin https://github.com/yourusername/digiflashcards.gitgit push -u origin mainStep 9: Deploy PostgreSQL Database on Klutch.sh
import { Steps } from ‘@astrojs/starlight/components’;
-
Navigate to your Klutch.sh dashboard
-
Click Create New App and select your GitHub repository
-
Configure the database deployment:
- Name:
digiflashcards-db - Source: Select
postgres:15-alpinefrom Docker Hub - Traffic Type: Select TCP (PostgreSQL requires TCP traffic)
- Internal Port:
5432(default PostgreSQL port) - External Port: Your database will be accessible on port
8000
- Name:
-
Add a Persistent Volume for database storage:
- Mount Path:
/var/lib/postgresql/data - Size:
20GB(adjust based on expected number of flashcards)
- Mount Path:
-
Set environment variables for PostgreSQL:
POSTGRES_DB:digiflashcardsPOSTGRES_USER:postgresPOSTGRES_PASSWORD:your-secure-password(use a strong password)
-
Click Deploy and wait for the database to be ready
-
Note your database connection details:
- Host:
your-database-app.klutch.sh - Port:
8000(external access) - Database:
digiflashcards - User:
postgres - Password: Your configured password
- Host:
Step 10: Deploy Digiflashcards Application on Klutch.sh
-
Return to your Klutch.sh dashboard
-
Click Create New App and select your Digiflashcards GitHub repository
-
Configure the application deployment:
- Name:
digiflashcards-app - Traffic Type: Select HTTP (web application)
- Internal Port:
3000(Digiflashcards runs on port 3000)
- Name:
-
Add Persistent Volumes for media storage:
Volume 1 - Image Uploads:
- Mount Path:
/app/uploads/images - Size:
20GB(stores card images and deck covers)
Volume 2 - Audio Uploads:
- Mount Path:
/app/uploads/audio - Size:
10GB(stores audio clips for language learning)
Volume 3 - Application Logs:
- Mount Path:
/app/logs - Size:
5GB(stores application and study session logs)
- Mount Path:
-
Configure environment variables:
NODE_ENV:productionPORT:3000DB_HOST:your-database-app.klutch.sh(from Step 9)DB_PORT:8000DB_NAME:digiflashcardsDB_USER:postgresDB_PASSWORD: Your database passwordJWT_SECRET: Generate a secure random stringDEFAULT_EASE_FACTOR:2.5NEW_CARDS_PER_DAY:20CORS_ORIGIN:*(or your specific domain)ENABLE_PUBLIC_DECKS:trueENABLE_DECK_SHARING:true
-
Click Deploy
-
Klutch.sh will automatically:
- Detect your Dockerfile in the repository root
- Build the Docker image with Node.js dependencies
- Deploy your application with the configured volumes
- Assign a URL like
digiflashcards-app.klutch.sh
-
Once deployed, verify the deployment by visiting:
https://digiflashcards-app.klutch.sh/health
Step 11: Initial Setup and Configuration
After deployment, perform the initial setup:
-
Create Admin User: Use the API to create your first admin user:
Terminal window curl -X POST https://digiflashcards-app.klutch.sh/api/users/register \-H "Content-Type: application/json" \-d '{"username": "admin","email": "admin@example.com","password": "secure-password","full_name": "Admin User"}' -
Create First Deck: Create a test deck to verify functionality:
Terminal window curl -X POST https://digiflashcards-app.klutch.sh/api/decks \-H "Authorization: Bearer YOUR_JWT_TOKEN" \-H "Content-Type: application/json" \-d '{"name": "Getting Started","description": "Learn the basics of Digiflashcards","is_public": false,"tags": ["tutorial", "basics"]}' -
Add Sample Cards: Create flashcards in your deck:
Terminal window curl -X POST https://digiflashcards-app.klutch.sh/api/cards \-H "Authorization: Bearer YOUR_JWT_TOKEN" \-H "Content-Type: application/json" \-d '{"deck_id": 1,"front_content": "What is spaced repetition?","back_content": "A learning technique that incorporates increasing intervals of time between reviews of previously learned material","card_type": "basic","tags": ["learning", "memory"]}' -
Test Study Session: Start a study session to verify the spaced repetition algorithm:
Terminal window curl -X POST https://digiflashcards-app.klutch.sh/api/study/start \-H "Authorization: Bearer YOUR_JWT_TOKEN" \-H "Content-Type: application/json" \-d '{"deck_id": 1}' -
Configure Daily Goals: Set your daily study goals through the settings API
API Reference
Deck Management Endpoints
Create Deck
POST /api/decksAuthorization: Bearer TOKENContent-Type: application/json
{ "name": "Spanish Vocabulary", "description": "Common Spanish words and phrases", "cover_image": "https://example.com/cover.jpg", "is_public": true, "tags": ["spanish", "language", "beginner"]}
Response:{ "success": true, "deck": { "id": 1, "name": "Spanish Vocabulary", "description": "Common Spanish words and phrases", "card_count": 0, "created_at": "2025-12-15T10:00:00Z" }}Get Decks
GET /api/decks?user_id=1&is_public=trueAuthorization: Bearer TOKEN
Response:{ "decks": [ { "id": 1, "name": "Spanish Vocabulary", "card_count": 50, "study_count": 145, "last_studied": "2025-12-14T15:30:00Z" } ], "total": 1}Card Management Endpoints
Create Card
POST /api/cardsAuthorization: Bearer TOKENContent-Type: application/json
{ "deck_id": 1, "front_content": "Hola", "back_content": "Hello", "front_media": "/uploads/images/hola.jpg", "card_type": "basic", "tags": ["greeting", "common"]}
Response:{ "success": true, "card": { "id": 1, "deck_id": 1, "front_content": "Hola", "back_content": "Hello", "ease_factor": 2.5, "interval": 0, "next_review_date": null }}Bulk Import Cards
POST /api/cards/importAuthorization: Bearer TOKENContent-Type: multipart/form-data
Parameters:- deck_id: Deck ID- file: CSV file- format: "csv" or "anki" or "quizlet"
Response:{ "success": true, "cards_imported": 50, "cards_failed": 2, "import_id": 123}Study Session Endpoints
Start Study Session
POST /api/study/startAuthorization: Bearer TOKENContent-Type: application/json
{ "deck_id": 1, "session_type": "review", "card_limit": 20}
Response:{ "success": true, "session_id": 456, "due_cards": [ { "id": 1, "front_content": "Hola", "back_content": "Hello", "difficulty": "medium" } ], "total_due": 15}Submit Card Review
POST /api/study/reviewAuthorization: Bearer TOKENContent-Type: application/json
{ "session_id": 456, "card_id": 1, "rating": 4, "time_taken_seconds": 5}
Response:{ "success": true, "next_review_date": "2025-12-21T10:00:00Z", "new_interval": 6, "new_ease_factor": 2.6, "cards_remaining": 14}Statistics Endpoints
Get User Statistics
GET /api/users/statisticsAuthorization: Bearer TOKEN
Response:{ "total_cards_studied": 1523, "total_study_time_seconds": 45600, "current_streak_days": 15, "longest_streak_days": 42, "cards_due_today": 20, "cards_due_tomorrow": 15, "accuracy_rate": 87.5, "average_ease_factor": 2.4}Get Deck Statistics
GET /api/decks/1/statisticsAuthorization: Bearer TOKEN
Response:{ "deck_id": 1, "total_cards": 50, "cards_new": 10, "cards_learning": 15, "cards_mature": 25, "average_retention": 92.3, "study_sessions": 45, "total_reviews": 450}Production Best Practices
Security Hardening
- Strong Authentication: Implement rate limiting on login endpoints
- Content Sanitization: Always sanitize user-generated markdown content
- File Upload Validation: Validate file types and sizes strictly
- HTTPS Only: Force HTTPS in production
- JWT Rotation: Implement token refresh mechanism
- Input Validation: Use Joi or similar for all API inputs
Performance Optimization
- Database Indexing: Ensure proper indexes on frequently queried columns
- Caching: Implement Redis caching for deck and card data
- Image Optimization: Compress and resize uploaded images
- Lazy Loading: Load cards on-demand during study sessions
- Connection Pooling: Configure database connection pools appropriately
- CDN: Use a CDN for serving static assets and media files
Data Management
- Regular Backups: Automate database and volume backups daily
- Data Retention: Implement policies for old study session data
- Export Functionality: Allow users to export their decks and progress
- Soft Deletes: Implement soft deletes for cards and decks
- Audit Logging: Maintain logs of important user actions
Monitoring and Alerting
Set up monitoring for:
- Study session completion rates
- API response times and error rates
- Database query performance
- Storage usage for uploads
- Daily active users and study streaks
- Card review accuracy trends
Troubleshooting
Cards Not Appearing for Review
Symptoms: No cards showing up in study session despite cards being due
Solutions:
-
Check
next_review_datein database for cards -
Verify timezone settings match user’s timezone
-
Run query to count due cards:
SELECT COUNT(*) FROM cardsWHERE deck_id = 1AND (next_review_date IS NULL OR next_review_date <= NOW()); -
Ensure study session is using correct deck_id
-
Check if cards have been accidentally archived or deleted
Spaced Repetition Not Working Correctly
Symptoms: Cards appearing too frequently or infrequently
Solutions:
-
Verify SM-2 algorithm implementation in
sm2-algorithm.ts -
Check
ease_factorandintervalvalues in database -
Ensure review ratings are being properly recorded
-
Reset problematic cards with:
UPDATE cards SETease_factor = 2.5,interval = 0,repetitions = 0,next_review_date = NULLWHERE id = ?; -
Review
review_logstable for patterns in review history
Upload Failures
Symptoms: Image or audio uploads failing
Solutions:
- Check
MAX_UPLOAD_SIZEenvironment variable - Verify
/app/uploadsvolume has free space - Ensure file type is in
ALLOWED_IMAGE_TYPESorALLOWED_AUDIO_TYPES - Check file permissions on upload directories
- Verify multer middleware configuration
- Review nginx/proxy upload size limits if using reverse proxy
Database Connection Issues
Symptoms: Connection errors or timeouts
Solutions:
- Verify database credentials in environment variables
- Check database service is running
- Ensure database port 8000 is accessible
- Verify persistent volume is properly mounted
- Check database connection pool settings
- Review database logs for errors
Slow Performance During Study Sessions
Symptoms: Lag when loading cards or submitting reviews
Solutions:
-
Add indexes on frequently queried columns:
CREATE INDEX idx_cards_deck_next_reviewON cards(deck_id, next_review_date); -
Implement card prefetching in study sessions
-
Optimize card queries to fetch only necessary fields
-
Enable query result caching
-
Consider pagination for large decks
-
Profile slow queries with EXPLAIN ANALYZE
Import/Export Issues
Symptoms: CSV or Anki imports failing or data corruption
Solutions:
- Validate CSV format matches expected structure
- Check for special characters and encoding issues
- Ensure proper escaping of quotes and commas
- Verify file encoding is UTF-8
- Test with small sample files first
- Review import logs for specific error messages
Scaling Your Digiflashcards Deployment
As your flashcard platform grows, consider:
- Horizontal Scaling: Deploy multiple application instances behind a load balancer
- Database Optimization: Use PostgreSQL read replicas for statistics queries
- Redis Caching: Cache frequently accessed decks and user statistics
- Background Workers: Offload import/export processing to worker services
- CDN Integration: Serve media files through a CDN for better performance
- Search Optimization: Implement Elasticsearch for full-text card search
- Microservices: Split study sessions, statistics, and deck management into separate services
- Queue System: Use RabbitMQ or Redis for async processing of long-running tasks
Advanced Features
AI-Generated Flashcards
If you enable AI generation (ENABLE_AI_GENERATION=true), you can integrate with OpenAI or similar services:
import OpenAI from 'openai';
async function generateFlashcards(text: string, count: number = 10) { const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const completion = await openai.chat.completions.create({ model: "gpt-4", messages: [ { role: "system", content: "Generate flashcards from the provided text. Return JSON array with 'front' and 'back' fields." }, { role: "user", content: `Generate ${count} flashcards from this text:\n\n${text}` } ], response_format: { type: "json_object" } });
return JSON.parse(completion.choices[0].message.content);}Mobile App Integration
Digiflashcards’ API-first design makes it easy to build native mobile apps:
- Use JWT authentication for secure mobile sessions
- Implement offline sync with local SQLite database
- Sync review data when connection is restored
- Push notifications for daily study reminders
- Progressive Web App (PWA) support for installable web app
Conclusion
You’ve successfully deployed Digiflashcards on Klutch.sh! Your spaced repetition flashcard platform is now ready to help users learn and retain information more effectively through scientifically-proven study techniques.
For more deployment guides and platform documentation, visit the Klutch.sh documentation.