A modern, self-hosted Magic: The Gathering deck builder with multi-user support, API key authentication, and a beautiful web interface.
- Multi-User Support: Secure user registration and authentication with JWT tokens
- Admin System: Role-based access control with admin user management
- API Key Authentication: Generate and manage API keys for external integrations
- User Profiles: Gravatar integration with fallback to colorful initials
- User Statistics: Track deck count, card count, API keys, and shared decks
- Deck Builder: Intuitive drag-and-drop interface for building decks
- Multiple Printings: Choose specific card art, set, and foil versions
- Mainboard & Sideboard: Full support for mainboard and sideboard management
- Commander Support: Mark commanders for Commander format decks
- Deck Statistics: Real-time visual mana curve, type distribution, and color analysis
- Format Support: Standard, Modern, Commander, Legacy, Vintage, Pauper
- Deck Import/Export: Import and export decks in multiple formats:
- Moxfield format with set codes
- MTG Arena format
- MTGO format
- Plain text format
- Fast Card Search: Real-time autocomplete search as you type
- Advanced Filtering: Filter by colors, card types, CMC range, and sets
- Smart Sorting: Sort by name, mana value, color, price, or random
- Set Browser: Browse cards by specific Magic sets
- Price Tracking: TCGplayer pricing with low/mid/market values
- Card Preview: Hover to see full card images
- Scryfall Integration: Scryfall IDs for all printings
- Type Arrays: Separate supertypes, types, and subtypes
- Related Cards: Track card relationships (tokens, meld pairs, etc.)
- Commander Data: Leadership skills for Commander format
- Identifiers: Multiple card identifiers (MTGO, TCGplayer, etc.)
- Foreign Data: Multi-language card names and text
- EDHREC Metadata: Commander popularity and ranking data
- Public Sharing: Share decks with unique public URLs
- Import Shared Decks: One-click import of shared decks
- Read-Only Views: Public viewers can see deck lists and stats
- Price Display: Real-time pricing for each card and total deck value
- Mass Entry: Export decks directly to TCGplayer with set codes
- Copy Deck Lists: Format deck lists for manual paste
- User Management: View, edit, promote/demote, and delete users
- Backup & Restore: Export and import all user data (decks, cards, API keys)
- Database Sync: Manual refresh of MTGJSON card data and pricing
- Safe Reimport: FORCE_REIMPORT preserves user decks using UUIDs
- Self-Hosted: Own your data with SQLite database
- Dockerized: Easy deployment with Docker and docker-compose
- Database Flexibility: Built with abstraction layer for future database options
- Auto-Sync: Daily automatic card data and pricing updates at 3 AM
- Node.js + Express
- SQLite with better-sqlite3
- JWT authentication + API key support
- bcrypt password hashing
- Compression, rate limiting, security headers
- Vanilla JavaScript (ES6+)
- Vite for build and dev server
- Modern CSS with custom properties
- Responsive design
- Real-time search with debouncing
- MTGJSON for comprehensive Magic card data
- All card printings with artist info and set details
The fastest way to get Deck Lotus running is to use our pre-built Docker image:
# Create a directory for your data
mkdir -p deck-lotus-data
# Create a docker-compose.yml file
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
deck-lotus:
image: ghcr.io/madeofpendletonwool/deck-lotus:latest
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- JWT_SECRET=your-super-secret-random-string-here-at-least-64-characters-long
- ADMIN_USERNAME=admin
- [email protected]
- ADMIN_PASSWORD=changeme123
volumes:
- ./deck-lotus-data:/app/data
restart: unless-stopped
EOF
# Edit docker-compose.yml and set a secure JWT_SECRET
nano docker-compose.yml
# Start Deck Lotus
docker-compose up -dThe app will be available at http://localhost:3000
Important: Change the JWT_SECRET to a secure random string before starting! Generate one with:
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"If you prefer to build the Docker image yourself from source:
# Clone the repository
git clone https://github.com/your-username/deck-lotus.git
cd deck-lotus
# Copy environment file
cp .env.example .env
# Edit .env and set a secure JWT_SECRET
nano .env
# Build and run with Docker Compose
docker-compose up -dThe app will be available at http://localhost:3000
- Navigate to
http://localhost:3000 - Click "Register" on the login page
- Enter username, email, and password (min 8 characters)
- You'll be automatically logged in
- Click "New Deck" on the My Decks page
- Enter a deck name and select a format (optional)
- Search for cards using the search box (autocomplete appears as you type)
- Click a card to add it to your deck
- Use +/- buttons to adjust quantities
- Toggle between Mainboard and Sideboard tabs
- View live statistics (mana curve, card types, colors)
- View real-time deck pricing with TCGplayer integration
- Click "Save" to save your changes
Import decks from other platforms:
- Click "Import Deck" on the My Decks page
- Enter a deck name and format
- Paste your deck list in any of these formats:
1 Lightning Bolt(Arena/MTGO)4 Counterspell [DMR](Moxfield with set codes)1 Black Lotus (LEA)(set codes in parentheses)
- Click "Import Deck"
The importer intelligently detects format and matches cards to printings.
Export decks to various formats:
- Open a deck
- Click the export icon (download button) in the deck tabs
- Choose your format:
- Moxfield: Includes set codes and collector numbers
- Arena: Simple format for MTG Arena
- MTGO: Compatible with Magic Online
- Plain Text: Simple quantity + name format
- Click "Copy to Clipboard" or manually copy the text
Share decks with friends or the community:
- Open a deck
- Click "Share" button
- Copy the generated public URL
- Anyone with the link can view (read-only)
- Viewers can import shared decks to their account
To stop sharing:
- Open the deck
- Click "Share" again
- Click "Delete Share Link"
Explore the card database with advanced filtering:
- Go to "Browse Cards" page
- Use the search box for quick name search
- Apply filters:
- Sort: Alphabetical, mana value, color, price, random
- Type: Creature, Instant, Sorcery, Enchantment, etc.
- CMC: Set min/max mana value range
- Colors: Select color combinations (W, U, B, R, G, C)
- Sets: Filter by specific Magic sets
- Hover over cards to see full preview images
- Click cards to view all printings and prices
Purchase your decks via TCGplayer:
- Open a deck
- Click "Buy Deck" button
- Choose TCGplayer Mass Entry (recommended)
- Deck list with set codes opens in TCGplayer
- Review prices and add to cart
Alternatively, use Copy Deck List to manually paste elsewhere.
Your user profile displays:
- Gravatar: If you have a Gravatar associated with your email, it displays automatically
- Username & Email: Your account details
- Statistics:
- Total decks created
- Total cards across all decks
- API keys generated
- Shared decks (publicly accessible)
Access your profile by clicking your avatar in the top-right corner.
Generate API keys for external integrations:
- Go to Settings page
- Click "Generate New API Key"
- Enter a name for the key
- Save the generated key (shown only once!)
Use API keys in requests:
curl -H "X-API-Key: your-api-key-here" http://localhost:3000/api/cards/search?q=lightningIf you're an admin user, you have access to additional features in Settings:
User Management:
- View all registered users
- Promote users to admin or demote from admin
- Delete user accounts (removes all their decks and data)
- Cannot remove your own admin status or delete your own account
Database Management:
- Manually trigger MTGJSON sync to update card data and pricing
- View last sync timestamp
- Auto-sync runs daily at 3:00 AM
Backup & Restore:
- Export all user data (users, decks, API keys, shares)
- Import previously exported backups
- Choose to overwrite or merge data
- Backups preserve deck integrity using UUIDs
POST /api/auth/register
Content-Type: application/json
{
"username": "player1",
"email": "[email protected]",
"password": "securepass123"
}POST /api/auth/login
Content-Type: application/json
{
"username": "player1",
"password": "securepass123"
}GET /api/auth/me
Authorization: Bearer <jwt-token>GET /api/auth/stats
Authorization: Bearer <jwt-token>Returns: { stats: { deckCount, cardCount, apiKeyCount, sharedDeckCount } }
POST /api/auth/api-keys
Authorization: Bearer <jwt-token>
Content-Type: application/json
{
"name": "My Integration"
}GET /api/auth/api-keys
Authorization: Bearer <jwt-token>DELETE /api/auth/api-keys/:id
Authorization: Bearer <jwt-token>All card endpoints require authentication (JWT or API Key).
GET /api/cards/search?q=lightning&limit=20
Authorization: Bearer <jwt-token>GET /api/cards/browse?name=bolt&colors=R&type=Instant&cmcMin=1&cmcMax=3&sets=MH2&sort=name&page=1&limit=20
Authorization: Bearer <jwt-token>Query parameters:
name: Card name searchcolors: Color filter (e.g.,W,UB,WUG)type: Card type filtercmcMin/cmcMax: Mana value rangesets: Comma-separated set codessort:name,cmc,color,price,randompage: Page number (default: 1)limit: Results per page (default: 20)
GET /api/cards/:id
Authorization: Bearer <jwt-token>GET /api/cards/:id/printings
Authorization: Bearer <jwt-token>Returns all printings of a card with set info, prices, and availability.
GET /api/decks
Authorization: Bearer <jwt-token>POST /api/decks
Authorization: Bearer <jwt-token>
Content-Type: application/json
{
"name": "My Red Deck",
"format": "standard",
"description": "Aggressive red deck"
}GET /api/decks/:id
Authorization: Bearer <jwt-token>PUT /api/decks/:id
Authorization: Bearer <jwt-token>
Content-Type: application/json
{
"name": "Updated Name",
"format": "modern"
}DELETE /api/decks/:id
Authorization: Bearer <jwt-token>POST /api/decks/:id/cards
Authorization: Bearer <jwt-token>
Content-Type: application/json
{
"printingId": 123,
"quantity": 4,
"isSideboard": false
}PUT /api/decks/:id/cards/:cardId
Authorization: Bearer <jwt-token>
Content-Type: application/json
{
"quantity": 2
}DELETE /api/decks/:id/cards/:cardId
Authorization: Bearer <jwt-token>GET /api/decks/:id/stats
Authorization: Bearer <jwt-token>Returns mana curve, type distribution, and color breakdown.
GET /api/decks/:id/price
Authorization: Bearer <jwt-token>Returns TCGplayer pricing for the entire deck (low, mid, market).
POST /api/decks/import
Authorization: Bearer <jwt-token>
Content-Type: application/json
{
"name": "Imported Deck",
"format": "commander",
"deckList": "1 Lightning Bolt\n4 Counterspell [DMR]\n..."
}Supports multiple formats: Arena, MTGO, Moxfield (with set codes).
POST /api/decks/:id/share
Authorization: Bearer <jwt-token>Returns: { token: "uuid", shareUrl: "http://..." }
DELETE /api/decks/:id/share
Authorization: Bearer <jwt-token>GET /api/decks/share/:tokenNo authentication required. Returns deck details, cards, and statistics.
POST /api/decks/share/:token/import
Authorization: Bearer <jwt-token>Creates a copy of the shared deck in your account.
GET /api/sets
Authorization: Bearer <jwt-token>Returns all Magic sets with metadata (name, code, release date, type).
GET /api/sets/:code
Authorization: Bearer <jwt-token>GET /api/sets/:code/cards?page=1
Authorization: Bearer <jwt-token>Returns paginated cards from a specific set.
All admin endpoints require authentication AND admin privileges.
GET /api/admin/users
Authorization: Bearer <jwt-token>Admin only. Returns all registered users with their roles.
PUT /api/admin/users/:id
Authorization: Bearer <jwt-token>
Content-Type: application/json
{
"is_admin": 1
}Admin only. Update user details or admin status. Cannot remove own admin status.
DELETE /api/admin/users/:id
Authorization: Bearer <jwt-token>Admin only. Delete user and all their data (decks, cards, API keys). Cannot delete own account.
POST /api/admin/backup
Authorization: Bearer <jwt-token>Admin only. Creates JSON backup of all user data (users, decks, cards, API keys, shares).
POST /api/admin/restore
Authorization: Bearer <jwt-token>
Content-Type: application/json
{
"backup": { ... },
"overwrite": false
}Admin only. Restores data from backup. overwrite: true replaces all data, false merges.
POST /api/admin/sync
Authorization: Bearer <jwt-token>Admin only. Manually trigger MTGJSON sync to update card data and pricing.
GET /api/admin/sync-status
Authorization: Bearer <jwt-token>Admin only. Returns last sync timestamp and current sync status.
deck-lotus/
├── src/
│ ├── db/
│ │ ├── migrations/ # Database migrations
│ │ ├── connection.js # Database abstraction layer
│ │ └── index.js
│ ├── middleware/
│ │ ├── auth.js # JWT + API key authentication
│ │ └── errorHandler.js
│ ├── services/
│ │ ├── authService.js # User authentication logic
│ │ ├── cardService.js # Card search and retrieval
│ │ └── deckService.js # Deck management
│ ├── routes/
│ │ ├── auth.js # Auth endpoints
│ │ ├── cards.js # Card endpoints
│ │ └── decks.js # Deck endpoints
│ ├── utils/
│ │ ├── jwt.js # JWT utilities
│ │ └── validators.js # Input validation
│ └── server.js # Express server
├── client/
│ ├── src/
│ │ ├── components/ # UI components
│ │ ├── services/ # API client
│ │ ├── styles/ # CSS
│ │ ├── utils/ # Helper functions
│ │ └── main.js # App entry point
│ ├── index.html
│ ├── vite.config.js
│ └── package.json
├── scripts/
│ ├── import-mtgjson.js # Import card data
│ └── init-db.js # Initialize database
├── data/ # SQLite database (gitignored)
├── Dockerfile # Multi-stage Docker build
├── docker-compose.yml # Docker Compose configuration
├── .dockerignore
├── .env.example
└── package.json
- Authentication and user management
- Hashed passwords with bcrypt (10 rounds)
- Admin flag for role-based access control
- Email for Gravatar integration
- Created timestamp
- API key generation and management
- SHA-256 hashed keys with prefixes
- Named keys for easy identification
- Last used tracking
- User ownership
- Atomic card data (shared across all printings)
- Name, mana cost, colors, types, rules text
- Enhanced fields:
supertypes,types,subtypes(JSON arrays)leadership(JSON object for Commander skills)- Power/toughness, loyalty
- Keywords and abilities
- EDHREC rank and salt score
- Set-specific card data
- Artist, collector number, set code, rarity
- Multiple printings per card
- Enhanced fields:
uuid(stable MTGJSON identifier)scryfallIdfor Scryfall integrationidentifiers(JSON: mtgoId, tcgplayerId, etc.)availability(paper, arena, mtgo)foreignData(JSON array of translations)relatedCards(JSON: tokens, meld pairs, etc.)isFoil,isNonFoil,isPromoframeVersion,borderColor
- TCGplayer pricing data (low, mid, market)
- Foil and non-foil prices
- Updated daily via auto-sync
- Historical tracking ready
- Magic set information
- Set name, code, release date
- Type (core, expansion, masters, etc.)
- Block information
- User's deck metadata
- Format, name, description
- Foreign key to users (CASCADE delete)
- Created/updated timestamps
- Junction table for deck composition
- Quantity, mainboard/sideboard flag
- Commander flag for Commander format
- References specific printing by UUID-mapped ID
- Foreign key CASCADE ensures data integrity
- Public sharing tokens (UUID v4)
- Active/inactive status
- Created timestamp
- Foreign key to decks
The database includes 12 migrations:
- Initial schema (users, cards, printings, decks, deck_cards, api_keys)
- Pricing and sets tables
- Rulings table
- Deck sharing functionality
- Scryfall IDs
- Type arrays (supertypes, types, subtypes)
- Related cards (tokens, transforms, etc.)
- Leadership skills (Commander)
- Identifiers (MTGO, TCGplayer, etc.)
- Foreign data (translations)
- EDHREC metadata
- Admin users flag
All migrations are idempotent and tracked in schema_migrations table.
- Backend: Add routes in
src/routes/, services insrc/services/ - Frontend: Add components in
client/src/components/ - Database: Create migration in
src/db/migrations/
# Backend tests (when available)
npm test
# Frontend tests (when available)
cd client && npm testThe Dockerfile uses multi-stage builds to minimize image size:
- Frontend Builder: Builds the Vite frontend
- Backend Builder: Installs production dependencies
- Final Image: Alpine-based Node.js with only runtime files
Final image size: ~150MB (compared to ~800MB without optimization)
Deck Lotus automatically ensures there's always at least one admin user.
On every startup, the system checks:
- If an admin user exists: Nothing happens, server starts normally
- If NO admin user exists:
- With environment variables set: Creates admin from
ADMIN_USERNAME,ADMIN_EMAIL,ADMIN_PASSWORD - Without environment variables: Auto-generates random credentials and displays them in the terminal
- With environment variables set: Creates admin from
# Set these in docker-compose.yml or .env
ADMIN_USERNAME=admin
[email protected]
ADMIN_PASSWORD=changeme123If you don't set the environment variables and no admin exists, you'll see:
╔════════════════════════════════════════════════════════════╗
║ AUTO-GENERATED ADMIN CREDENTIALS ║
╠════════════════════════════════════════════════════════════╣
║ Username: admin ║
║ Email: admin@localhost ║
║ Password: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 ║
╠════════════════════════════════════════════════════════════╣
║ ⚠️ SAVE THESE CREDENTIALS NOW! ║
║ They will not be shown again. ║
║ Change the password after first login! ║
╚════════════════════════════════════════════════════════════╝
Important: The password is randomly generated and shown ONLY ONCE. Save it immediately!
If you're upgrading from a version before admin functionality:
- Set
ADMIN_USERNAMEto your existing username in docker-compose.yml - The system will automatically promote that user to admin on next startup
- If the user doesn't exist, it will be created with the specified credentials
- Go to Settings > User Management
- View all users, promote/demote admins, delete accounts
- Use Backup & Restore to export/import all user data
- Change the admin password immediately!
Security Note: Always change default or auto-generated passwords on first login!
Admins can backup and restore all user data from the Settings page.
- Log in as an admin user
- Go to Settings > Backup & Restore
- Click Download Backup
- Save the JSON file to a safe location
Backups include:
- All users and their credentials
- All decks and deck cards (using stable UUIDs)
- API keys
- Deck sharing tokens
- Admin status for each user
Note: Backups do NOT include card database (MTGJSON data) - that's auto-imported.
- Log in as an admin user
- Go to Settings > Backup & Restore
- Click Restore from Backup
- Select your backup JSON file
- Choose whether to:
- Overwrite existing data (replaces everything)
- Merge with existing data (may create duplicates)
- Review the restore results
After restoration, decks will automatically link to the correct card printings using UUIDs.
Complete list of environment variables you can set in .env or docker-compose.yml:
| Variable | Default | Required | Description |
|---|---|---|---|
PORT |
3000 |
No | Port for the web server to listen on |
NODE_ENV |
development |
No | Environment mode (development or production) |
DATABASE_PATH |
./data/deck-lotus.db |
No | Path to SQLite database file |
JWT_SECRET |
- | Yes | Secret key for signing JWT tokens (use a long random string) |
JWT_EXPIRES_IN |
7d |
No | How long JWT access tokens are valid (e.g., 7d, 24h) |
JWT_REFRESH_EXPIRES_IN |
30d |
No | How long refresh tokens are valid |
MTGJSON_URL |
(auto-detected) | No | Custom MTGJSON download URL (rarely needed) |
FORCE_REIMPORT |
false |
No | Force complete database reimport on startup. Set to true to clear and reimport all MTGJSON data. User decks are automatically preserved using UUIDs! |
ADMIN_USERNAME |
- | No | Username for admin account. If no admin exists, creates or promotes this user to admin. If not set and no admin exists, auto-generates credentials. |
ADMIN_EMAIL |
- | No | Email for admin account (only used when creating new admin) |
ADMIN_PASSWORD |
- | No | Password for admin account (only used when creating new admin). Change immediately after first login! |
JWT_SECRET: This is the only required variable. Generate a secure random string:
# Generate a secure JWT secret
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"FORCE_REIMPORT: When set to true, completely clears and reimports all card data from MTGJSON. This is useful for:
- Getting the latest card data and pricing
- Fixing corrupted card database
- Updating to new MTGJSON schema versions
Important: User decks, users, and API keys are automatically backed up and restored using stable UUIDs, so you won't lose your data!
Admin Credentials: The system ensures at least one admin always exists:
- On every startup, checks if ANY admin user exists
- If no admin exists:
- If
ADMIN_USERNAME,ADMIN_EMAIL,ADMIN_PASSWORDare set → Creates/promotes that user - If variables not set → Auto-generates random credentials and displays them in terminal (ONCE only!)
- If
- If admin exists → Skips admin creation
This means you'll never be locked out - if you accidentally remove all admins, restart the container to create a new one.
After first login with default/generated credentials, immediately change the password via Settings!
# Server Configuration
PORT=3000
NODE_ENV=production
# Database
DATABASE_PATH=./data/deck-lotus.db
# Security (REQUIRED - change this!)
JWT_SECRET=your-super-secret-random-string-here-at-least-64-characters-long
JWT_EXPIRES_IN=7d
JWT_REFRESH_EXPIRES_IN=30d
# Admin User (created on first startup)
ADMIN_USERNAME=admin
ADMIN_EMAIL=[email protected]
ADMIN_PASSWORD=changeme123
# Database Import (optional)
FORCE_REIMPORT=falseversion: '3.8'
services:
deck-lotus:
image: deck-lotus:latest
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- JWT_SECRET=${JWT_SECRET:-your-secret-key-here}
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
- ADMIN_EMAIL=${ADMIN_EMAIL:[email protected]}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-changeme123}
- FORCE_REIMPORT=${FORCE_REIMPORT:-false}
volumes:
- ./data:/app/data
restart: unless-stoppedContributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
MIT License - see LICENSE file for details
- Card data provided by MTGJSON
- Built with Node.js, Express, and Vite
User Experience:
- ✨ User profile with Gravatar support and colorful initials fallback
- 📊 User statistics dashboard (deck count, card count, API keys, shared decks)
- 🎨 Modern animated dropdown menu for user profile
- 🔄 Real-time stats updates
Deck Management:
- 📥 Import decks from Moxfield, Arena, MTGO, and plain text formats
- 📤 Export decks to multiple formats with set codes
- 🔗 Share decks publicly with unique URLs
- 💰 TCGplayer integration for deck pricing and purchasing
- ⚡ Improved deck builder performance
Card Browsing:
- 🔍 Advanced filtering system (colors, types, CMC, sets)
- 📑 Smart sorting options (name, CMC, color, price, random)
- 🖼️ Card preview on hover
- 💵 Real-time price display for all printings
Enhanced Card Data (MTGJSON):
- 🆔 Scryfall IDs for all printings
- 📋 Structured type arrays (supertypes, types, subtypes)
- 🔗 Related cards tracking (tokens, transforms, meld pairs)
- 👑 Commander leadership skills
- 🌍 Foreign language data
- 📈 EDHREC metadata (rank, salt score)
- 🎮 Multiple identifiers (MTGO, TCGplayer, etc.)
Admin Features:
- 👥 User management (view, promote/demote, delete)
- 💾 Backup & restore all user data
- 🔄 Manual database sync trigger
- 🔐 Protected admin endpoints
Technical Improvements:
- ✅ UUID-based deck preservation during FORCE_REIMPORT
- 🏗️ 12 database migrations for enhanced functionality
- 🔒 Admin middleware protection
- 📦 Improved error handling
- ⚡ Performance optimizations
For issues and questions:
- Open an issue on GitHub
- Check existing issues for solutions
Planned features for future releases:
- 🎯 Deck recommendations and suggestions
- 📈 Price history tracking and alerts
- 🏆 Tournament tracking
- 📊 Advanced deck analytics
- 🎲 Goldfish playtesting
Made with love for the Magic: The Gathering community.