Skip to content

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 size
FROM node:20-alpine AS builder
# Install build dependencies
RUN apk add --no-cache \
python3 \
make \
g++ \
cairo-dev \
jpeg-dev \
pango-dev \
giflib-dev
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY tsconfig.json ./
# Install dependencies
RUN npm ci --only=production && \
npm cache clean --force
# Copy application source
COPY . .
# Build TypeScript application
RUN npm run build
# Production stage
FROM node:20-alpine
# Install runtime dependencies
RUN apk add --no-cache \
cairo \
jpeg \
pango \
giflib \
ffmpeg
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Set working directory
WORKDIR /app
# Copy built application from builder
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
# Create directories for uploads
RUN mkdir -p /app/uploads/images /app/uploads/audio /app/uploads/temp && \
chown -R nodejs:nodejs /app/uploads
# Switch to non-root user
USER nodejs
# Expose application port
EXPOSE 3000
# Health check
HEALTHCHECK --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 application
CMD ["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 connection
export 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,
});
// Middleware
app.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 uploads
app.use('/uploads', express.static('uploads'));
// API Routes
app.use('/api/decks', deckRoutes);
app.use('/api/cards', cardRoutes);
app.use('/api/study', studyRoutes);
app.use('/api/users', userRoutes);
// Health check endpoint
app.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 endpoint
app.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 handler
app.use((req: Request, res: Response) => {
res.status(404).json({
error: 'Not Found',
message: 'The requested resource does not exist',
});
});
// Error handler
app.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 application
async 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 shutdown
process.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:

Terminal window
# Server Configuration
NODE_ENV=production
PORT=3000
# Database Configuration
DB_HOST=your-database-host
DB_PORT=5432
DB_NAME=digiflashcards
DB_USER=postgres
DB_PASSWORD=your-secure-password
# Authentication Configuration
JWT_SECRET=your-jwt-secret-key-change-this
JWT_EXPIRES_IN=7d
# Upload Configuration
UPLOAD_DIR=/app/uploads
MAX_UPLOAD_SIZE=5242880
ALLOWED_IMAGE_TYPES=image/jpeg,image/png,image/gif,image/webp
ALLOWED_AUDIO_TYPES=audio/mpeg,audio/wav,audio/ogg
# CORS Configuration
CORS_ORIGIN=*
# Spaced Repetition Configuration
DEFAULT_EASE_FACTOR=2.5
MIN_EASE_FACTOR=1.3
MAX_INTERVAL_DAYS=365
NEW_CARDS_PER_DAY=20
# Email Configuration (optional)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
SMTP_FROM=noreply@digiflashcards.com
# Feature Flags
ENABLE_PUBLIC_DECKS=true
ENABLE_DECK_SHARING=true
ENABLE_IMPORT_EXPORT=true
ENABLE_AI_GENERATION=false
# Analytics Configuration
ANALYTICS_ENABLED=false
ANALYTICS_PROVIDER=
# Logging Configuration
LOG_LEVEL=info
LOG_TO_FILE=true
LOG_DIR=/app/logs

Step 8: Push to GitHub

Initialize a Git repository and push your code:

Terminal window
git init
git add .
git commit -m "Initial Digiflashcards deployment setup"
git branch -M main
git remote add origin https://github.com/yourusername/digiflashcards.git
git push -u origin main

Step 9: Deploy PostgreSQL Database on Klutch.sh

import { Steps } from ‘@astrojs/starlight/components’;

    1. Navigate to your Klutch.sh dashboard

    2. Click Create New App and select your GitHub repository

    3. Configure the database deployment:

      • Name: digiflashcards-db
      • Source: Select postgres:15-alpine from 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
    4. Add a Persistent Volume for database storage:

      • Mount Path: /var/lib/postgresql/data
      • Size: 20GB (adjust based on expected number of flashcards)
    5. Set environment variables for PostgreSQL:

      • POSTGRES_DB: digiflashcards
      • POSTGRES_USER: postgres
      • POSTGRES_PASSWORD: your-secure-password (use a strong password)
    6. Click Deploy and wait for the database to be ready

    7. Note your database connection details:

      • Host: your-database-app.klutch.sh
      • Port: 8000 (external access)
      • Database: digiflashcards
      • User: postgres
      • Password: Your configured password

Step 10: Deploy Digiflashcards Application on Klutch.sh

    1. Return to your Klutch.sh dashboard

    2. Click Create New App and select your Digiflashcards GitHub repository

    3. Configure the application deployment:

      • Name: digiflashcards-app
      • Traffic Type: Select HTTP (web application)
      • Internal Port: 3000 (Digiflashcards runs on port 3000)
    4. 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)
    5. Configure environment variables:

      • NODE_ENV: production
      • PORT: 3000
      • DB_HOST: your-database-app.klutch.sh (from Step 9)
      • DB_PORT: 8000
      • DB_NAME: digiflashcards
      • DB_USER: postgres
      • DB_PASSWORD: Your database password
      • JWT_SECRET: Generate a secure random string
      • DEFAULT_EASE_FACTOR: 2.5
      • NEW_CARDS_PER_DAY: 20
      • CORS_ORIGIN: * (or your specific domain)
      • ENABLE_PUBLIC_DECKS: true
      • ENABLE_DECK_SHARING: true
    6. Click Deploy

    7. 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
    8. 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:

    1. 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"
      }'
    2. 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"]
      }'
    3. 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"]
      }'
    4. 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
      }'
    5. Configure Daily Goals: Set your daily study goals through the settings API

API Reference

Deck Management Endpoints

Create Deck

POST /api/decks
Authorization: Bearer TOKEN
Content-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=true
Authorization: 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/cards
Authorization: Bearer TOKEN
Content-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/import
Authorization: Bearer TOKEN
Content-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/start
Authorization: Bearer TOKEN
Content-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/review
Authorization: Bearer TOKEN
Content-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/statistics
Authorization: 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/statistics
Authorization: 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

  1. Strong Authentication: Implement rate limiting on login endpoints
  2. Content Sanitization: Always sanitize user-generated markdown content
  3. File Upload Validation: Validate file types and sizes strictly
  4. HTTPS Only: Force HTTPS in production
  5. JWT Rotation: Implement token refresh mechanism
  6. Input Validation: Use Joi or similar for all API inputs

Performance Optimization

  1. Database Indexing: Ensure proper indexes on frequently queried columns
  2. Caching: Implement Redis caching for deck and card data
  3. Image Optimization: Compress and resize uploaded images
  4. Lazy Loading: Load cards on-demand during study sessions
  5. Connection Pooling: Configure database connection pools appropriately
  6. CDN: Use a CDN for serving static assets and media files

Data Management

  1. Regular Backups: Automate database and volume backups daily
  2. Data Retention: Implement policies for old study session data
  3. Export Functionality: Allow users to export their decks and progress
  4. Soft Deletes: Implement soft deletes for cards and decks
  5. 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:

  1. Check next_review_date in database for cards

  2. Verify timezone settings match user’s timezone

  3. Run query to count due cards:

    SELECT COUNT(*) FROM cards
    WHERE deck_id = 1
    AND (next_review_date IS NULL OR next_review_date <= NOW());
  4. Ensure study session is using correct deck_id

  5. Check if cards have been accidentally archived or deleted

Spaced Repetition Not Working Correctly

Symptoms: Cards appearing too frequently or infrequently

Solutions:

  1. Verify SM-2 algorithm implementation in sm2-algorithm.ts

  2. Check ease_factor and interval values in database

  3. Ensure review ratings are being properly recorded

  4. Reset problematic cards with:

    UPDATE cards SET
    ease_factor = 2.5,
    interval = 0,
    repetitions = 0,
    next_review_date = NULL
    WHERE id = ?;
  5. Review review_logs table for patterns in review history

Upload Failures

Symptoms: Image or audio uploads failing

Solutions:

  1. Check MAX_UPLOAD_SIZE environment variable
  2. Verify /app/uploads volume has free space
  3. Ensure file type is in ALLOWED_IMAGE_TYPES or ALLOWED_AUDIO_TYPES
  4. Check file permissions on upload directories
  5. Verify multer middleware configuration
  6. Review nginx/proxy upload size limits if using reverse proxy

Database Connection Issues

Symptoms: Connection errors or timeouts

Solutions:

  1. Verify database credentials in environment variables
  2. Check database service is running
  3. Ensure database port 8000 is accessible
  4. Verify persistent volume is properly mounted
  5. Check database connection pool settings
  6. Review database logs for errors

Slow Performance During Study Sessions

Symptoms: Lag when loading cards or submitting reviews

Solutions:

  1. Add indexes on frequently queried columns:

    CREATE INDEX idx_cards_deck_next_review
    ON cards(deck_id, next_review_date);
  2. Implement card prefetching in study sessions

  3. Optimize card queries to fetch only necessary fields

  4. Enable query result caching

  5. Consider pagination for large decks

  6. Profile slow queries with EXPLAIN ANALYZE

Import/Export Issues

Symptoms: CSV or Anki imports failing or data corruption

Solutions:

  1. Validate CSV format matches expected structure
  2. Check for special characters and encoding issues
  3. Ensure proper escaping of quotes and commas
  4. Verify file encoding is UTF-8
  5. Test with small sample files first
  6. Review import logs for specific error messages

Scaling Your Digiflashcards Deployment

As your flashcard platform grows, consider:

  1. Horizontal Scaling: Deploy multiple application instances behind a load balancer
  2. Database Optimization: Use PostgreSQL read replicas for statistics queries
  3. Redis Caching: Cache frequently accessed decks and user statistics
  4. Background Workers: Offload import/export processing to worker services
  5. CDN Integration: Serve media files through a CDN for better performance
  6. Search Optimization: Implement Elasticsearch for full-text card search
  7. Microservices: Split study sessions, statistics, and deck management into separate services
  8. 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.

Additional Resources