Project Overview - jra3/mulm GitHub Wiki
Project Overview
This document provides a high-level overview of the Mulm Breeder Awards Program platform architecture, technology stack, and design decisions.
What is Mulm?
Mulm is a web application for managing aquarium society Breeder Awards Programs (BAP), Horticultural Awards Programs (HAP), and Coral Awards Programs (CAP).
Production: https://bap.basny.org Repository: https://github.com/jra3/mulm
Core Features
- Member Management - User registration, authentication, profiles
- Submission System - Track breeding achievements with photos
- Approval Workflow - Multi-stage admin review with witness verification
- Awards & Levels - Automatic level advancement and specialty awards
- Species Catalog - Searchable database of fish, plants, and corals
- Activity Feed - Real-time member achievements
- Admin Tools - Queues, member management, reporting
Architecture Overview
High-Level Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β User Browser β
β (HTMX + Tailwind CSS) β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β HTTPS (nginx)
ββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββ
β Nginx (Reverse Proxy) β
β - SSL Termination (Let's Encrypt) β
β - Rate Limiting β
β - Static File Serving β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β HTTP (port 4200)
ββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββ
β Node.js / Express.js β
β β
β ββββββββββββββββ ββββββββββββββββ βββββββββββββββββββ β
β β Routes β β Business β β Utilities β β
β β (Express) ββββ Logic ββββ (Email, R2) β β
β ββββββββββββββββ ββββββββββββββββ βββββββββββββββββββ β
β β β
β ββββββββββββββββ ββββββββΌβββββββ βββββββββββββββββββ β
β β Session β β Database β β Validation β β
β β Manager β β Layer β β (Zod) β β
β ββββββββββββββββ ββββββββ¬βββββββ βββββββββββββββββββ β
ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββ
β
ββββββββββββββββ΄βββββββββββββββ
β β
βββββββββΌβββββββββ ββββββββββΌβββββββββ
β SQLite (DB) β β Cloudflare R2 β
β - Members β β - Images β
β - Submissions β β - Thumbnails β
β - Species β βββββββββββββββββββ
ββββββββββββββββββ
Technology Stack
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | Pug templates | Server-side rendered HTML |
| HTMX | Dynamic interactions without JavaScript framework | |
| Tailwind CSS | Utility-first styling | |
| PostCSS | CSS processing and optimization | |
| Backend | Node.js 20.x | Runtime environment |
| Express.js 5.x | Web application framework | |
| TypeScript | Type-safe JavaScript | |
| Database | SQLite 3.x | Embedded relational database |
| Node sqlite library | Async database interface | |
| Storage | Cloudflare R2 | S3-compatible object storage for images |
| Image Processing | Sharp (libvips) | High-performance image processing |
| Validation | Zod | Schema validation for forms |
| Authentication | Cookie sessions | Session-based authentication |
| OAuth 2.0 | Google sign-in | |
| scrypt | Password hashing | |
| Nodemailer | SMTP email sending | |
| Testing | Node.js test runner | Native test framework |
| tsx | TypeScript test execution | |
| Infrastructure | AWS EC2 | Application hosting |
| AWS EBS | Persistent storage | |
| Docker Compose | Container orchestration | |
| nginx | Reverse proxy and SSL termination | |
| Let's Encrypt | SSL certificates | |
| AWS CDK | Infrastructure as Code |
Design Decisions
Why SQLite?
Chosen over: PostgreSQL, MySQL, MongoDB
Reasons:
- β Simplicity - Single file, no separate server process
- β Performance - Excellent for read-heavy workloads (< 100K requests/day)
- β Reliability - Atomic commits, proven stability
- β Low overhead - Minimal resource usage
- β Easy backup - Single file to back up
- β Cost - No separate database hosting required
Limitations:
- β Single writer at a time (not an issue for our traffic)
- β No built-in replication (EBS snapshots provide backups)
- β Limited concurrency (acceptable for our use case)
When to migrate: If traffic exceeds 100K+ daily requests or need multi-region.
Why Pug + HTMX?
Chosen over: React, Vue, SPA frameworks
Reasons:
- β Server-side rendering - Better SEO, faster initial load
- β Simplicity - No build complexity for frontend
- β Progressive enhancement - Works without JavaScript
- β HTMX - Dynamic behavior without heavy JavaScript
- β Less complexity - No API layer needed for most features
- β Smaller bundle - Minimal JavaScript sent to client
Trade-offs:
- β Less rich interactions than SPA
- β Full page navigation (mitigated by HTMX)
- β Smaller ecosystem than React
Fits use case: Form-heavy CRUD application, not real-time collaborative editing.
Why TypeScript?
Chosen over: Plain JavaScript
Reasons:
- β Type safety - Catch errors at compile time
- β Better IDE support - Autocomplete, refactoring
- β Self-documenting - Types serve as inline documentation
- β Safer refactoring - Compiler catches breaking changes
- β Team scalability - Easier for new developers
Configuration: Strict mode enabled for maximum type safety.
Why Cloudflare R2?
Chosen over: AWS S3, local storage
Reasons:
- β No egress fees - S3-compatible but cheaper
- β S3 API - Easy to use AWS SDK
- β CDN integration - Fast global delivery
- β Reliability - Enterprise-grade storage
Cost comparison:
- AWS S3: $0.023/GB storage + egress fees
- Cloudflare R2: $0.015/GB storage + $0 egress
Why Docker Compose?
Chosen over: Kubernetes, bare metal, serverless
Reasons:
- β Right-sized - Not overengineered for single-server app
- β Simple - Easy to understand and maintain
- β Portable - Same config works locally and in production
- β Multi-container - App, nginx, certbot isolated
- β Easy rollback - Rebuild previous version quickly
When to migrate: If traffic requires multiple servers or auto-scaling.
Application Structure
Request Flow
1. Browser β HTTPS Request
2. nginx β Rate limiting, SSL termination
3. nginx β Proxy to Express (port 4200)
4. Express β Route matching
5. Route Handler β Database query
6. Database β Return data
7. Route Handler β Render Pug template
8. Express β Return HTML
9. nginx β Return to browser
10. Browser β Render page
Database Layer Pattern
Dual connection architecture:
// Read-only connection (for SELECT queries)
const readOnlyConn = await open({
filename: config.databaseFile,
mode: sqlite3.OPEN_READONLY,
});
// Write connection (for INSERT/UPDATE/DELETE)
const writeConn = await open({
filename: config.databaseFile,
mode: sqlite3.OPEN_READWRITE,
});
// Automatically select correct connection
function db(write = false) {
return write ? writeConn : readOnlyConn;
}
Benefits:
- Prevents accidental writes from read operations
- Better error messages (write fails on read-only conn)
- Clear intent in code
Transaction pattern:
await withTransaction(async (db) => {
// Multiple operations executed atomically
await db.run('INSERT INTO members ...');
await db.run('UPDATE submissions ...');
// Auto-commit on success, auto-rollback on error
});
Session Management
Cookie-based sessions:
// Stored in database (sessions table)
{
session_id: "random-uuid",
member_id: 123,
expires_on: "2025-11-01T00:00:00Z"
}
// Cookie sent to browser
Set-Cookie: session_id=random-uuid; HttpOnly; Secure; SameSite=Strict
Security features:
HttpOnly- Prevents JavaScript access (XSS protection)Secure- HTTPS only (production)SameSite=Strict- CSRF protection- Database-backed - Server-side session storage
Form Validation
Zod schemas define all forms:
// Define schema once
const submissionSchema = z.object({
species_common_name: z.string().min(2),
species_latin_name: z.string().min(3),
reproduction_date: z.string().datetime(),
// ... other fields
});
// Use for validation
const result = submissionSchema.safeParse(req.body);
if (!result.success) {
// Show errors to user
res.render('submit', { errors: result.error });
}
Benefits:
- Single source of truth for validation rules
- Type inference (TypeScript types from schemas)
- Runtime validation
- Clear error messages
Key Workflows
Submission Workflow
Member Creates Draft
β
Fills in Species Info
β
Fills in Tank Parameters
β
Uploads Photos (optional)
β
Adds Witness (if required)
β
Submits for Approval
β
ββββββββββββββββββββββββ
β Witness Required? β
ββββββββ¬βββββββββββ¬βββββ
βYes βNo
β β
Witness Queue Approval Queue
β β
Witness Confirms β
β β
ββββΌβββββββββββΌβββ
β Admin Reviews β
ββββ¬βββββββββββ¬βββ
βApprove βDeny/Changes
β β
Award Points Return to Member
β
Check Levels
β
Check Awards
β
Send Email
β
Activity Feed
Authentication Flow
User Visits Site
β
Click "Sign In"
β
ββββββββββββββββββββββββ
β Email/Password OR β
β Google OAuth β
ββββββββ¬βββββββββββ¬βββββ
β β
β β
Password Login OAuth Flow
β β
Verify scrypt Get Google Profile
β β
ββββΌβββββββββββΌβββ
β Create Session β
ββββββββββ¬βββββββββ
β
Set Cookie
β
Redirect Home
β
Authenticated!
Database Schema Overview
Core Tables
| Table | Purpose | Key Relationships |
|---|---|---|
| members | User accounts | Referenced by most tables |
| submissions | Breeding records | FK to members, species_name |
| species_name_group | Canonical species | Grouping synonyms |
| species_name | Species variants | FK to species_name_group |
| awards | Member awards | FK to members |
| tank_presets | Saved tank configs | FK to members |
| sessions | Active sessions | FK to members |
| auth_codes | Password reset codes | FK to members |
| activity_feed | Public activity log | FK to members |
| submission_notes | Admin notes | FK to submissions, members |
See: Database Schema for full ER diagrams.
Security Architecture
Defense in Depth
Layer 1: Network
- Nginx rate limiting (30 req/sec general, 5 req/sec uploads)
- Firewall (AWS Security Group)
- DDoS protection (Cloudflare if enabled)
Layer 2: Transport
- HTTPS only (Let's Encrypt TLS 1.2+)
- HSTS headers
- Secure cookies
Layer 3: Application
- Input validation (Zod schemas)
- SQL injection prevention (prepared statements)
- XSS prevention (Pug auto-escaping)
- CSRF protection (SameSite cookies)
Layer 4: Data
- Password hashing (scrypt)
- Config file permissions (600, owner-only)
- Database backups encrypted
- EXIF stripping from uploads
See: Security Overview for complete security posture.
Performance Characteristics
Benchmarks
Hardware: AWS t3.micro (2 vCPU, 1GB RAM)
| Operation | Response Time | Notes |
|---|---|---|
| Homepage | < 100ms | Static + activity feed |
| Member profile | < 150ms | DB query + rendering |
| Submission form | < 100ms | Cached tank presets |
| Search API | < 50ms | Indexed species search |
| Image upload | 2-5s | Processing + R2 upload |
| Admin queue | < 200ms | Filtered submissions |
Bottlenecks
Current:
- Single SQLite writer (acceptable for current traffic)
- Image processing CPU-bound (Sharp is fast enough)
- R2 upload network latency (5-10s for 5MB image)
Future optimizations if needed:
- Redis for session storage
- CDN for static assets
- Image upload to S3 Multipart
- Database connection pooling
Deployment Architecture
Production Environment
AWS EC2 Instance (t3.micro)
βββ Docker Compose
β βββ basny-app (Node.js)
β βββ basny-nginx (Reverse proxy)
β βββ basny-certbot (SSL renewal)
β
βββ /opt/basny (Application code)
β βββ Git repository
β
βββ /mnt/basny-data (EBS volume)
βββ app/
β βββ database/database.db
β βββ config/config.production.json
βββ nginx/
βββ certs/ (SSL certificates)
βββ logs/
CI/CD Pipeline
Currently: Manual deployment via SSH
Process:
- Push code to GitHub
- SSH to production
git pulllatest codedocker-compose up -d --build- Monitor logs
Future: GitHub Actions for automated testing and deployment.
Testing Strategy
Test Coverage
Unit Tests:
- Utilities (waiting period, status, image processing)
- Form validation
- Level calculation
Integration Tests:
- Database queries
- Species search
- Typeahead API
End-to-End Tests:
- Witness workflow
- Submission approval
- Upload transactions
Template Tests:
- Pug rendering
- Syntax validation
Current Status: 100% passing (all implemented tests)
See: Testing Guide
Development Workflow
Local Development
# Start dev server
npm run dev
# Server runs on http://localhost:4200
# Hot reload enabled for TypeScript and CSS
# Database auto-migrates on startup
Code Organization
src/
βββ routes/ # HTTP route handlers
βββ db/ # Database layer (queries)
βββ forms/ # Zod validation schemas
βββ utils/ # Helper functions
βββ views/ # Pug templates
βββ types/ # TypeScript type definitions
βββ __tests__/ # Test files
Principle: Separation of concerns
- Routes handle HTTP (thin layer)
- Database layer handles queries (reusable)
- Forms handle validation (single source of truth)
- Views handle presentation (no logic)
Roadmap & Future Enhancements
Potential Improvements
Short-term (3-6 months):
- Automated daily database backups to S3
- GitHub Actions CI/CD pipeline
- Email digest for admins (weekly summary)
- Improved mobile responsive design
Medium-term (6-12 months):
- Member-to-member messaging
- Advanced search filters
- Data export (CSV, PDF reports)
- Multi-language support
Long-term (1+ years):
- Mobile app (if needed)
- Multi-club support
- Live chat for members
- AI-assisted species identification
When to migrate tech:
- SQLite β PostgreSQL: If traffic > 100K req/day or need replication
- Single server β Multi-server: If need redundancy or auto-scaling
- Docker Compose β Kubernetes: If managing multiple applications
Related Documentation
- Development Setup - Get started developing
- Database Schema - Complete database documentation
- Testing Guide - How to write tests
- Production Deployment - Deployment procedures
- Infrastructure Guide - AWS resources and CDK
- CLAUDE.md - Comprehensive project documentation
**Last Updated: November 2025