Documentation for the Enigma puzzle collection
Enigma supports community-created puzzle packs that can include custom games, components, and even backend APIs. This guide covers how to install, manage, and create community packs.
The easiest way to install community packs is through the Game Store:
git@github.com:username/repo.githttps://github.com/username/repov1.2.0)Try the sample community pack:
git@github.com:ianfhunter/EnigmaSampleCommunityPack.git
When you add a GitHub repository URL, Enigma will:
v1.0.0)manifest.js from the latest tagged version (not main/master)Note: Repositories without any semver tags cannot be added. This ensures the metadata shown matches what will actually be installed.
Click the ✕ button on any source card to remove it. If the pack was installed, it will be uninstalled first.
When an update is available, the source card shows an orange "Update" badge:
For pack developers, Enigma supports local paths as sources for rapid iteration with hot-reload.
In the Game Store, you can add local paths instead of GitHub URLs:
file:///home/user/my-pack
/absolute/path/to/my-pack
./relative/path/to/my-pack
When you install a local source:
devYou can also manually create a symlink:
# Create symlink to your development pack
ln -s /path/to/my-pack-dev /path/to/Enigma/.plugins/my-pack-id
# The Vite watcher will auto-detect changes!
manifest.jsCommunity packs can import shared components, hooks, and utilities from the @enigma SDK alias.
import {
// Components
GameHeader, // Standard game header with back button
Timer, // Time display component
GiveUpButton, // Give up button with optional confirmation
StatsPanel, // Statistics display panel
GameResult, // Win/lose/gave-up result display
SeedDisplay, // Shows puzzle seed with copy/share buttons
DifficultySelector,
SizeSelector,
ModeSelector,
// Hooks
useTimer, // Timer logic with start/stop/reset
useGameStats, // Game stats with localStorage persistence
usePersistedState,// useState with localStorage
useKeyboardInput, // Keyboard event handling
useSeed, // Seed management with URL sync
// Utilities
renderIcon, // Render emoji or SVG icon
fuzzySearch, // Fuzzy string matching
createPackApi, // API client for pack backend
// Seeding (for reproducible puzzles)
createSeededRNG, // Seeded random number generator
seededShuffle, // Shuffle array with seed
seededChoice, // Pick random element with seed
seededSample, // Pick multiple random elements
seededInt, // Random integer with seed
seededFloat, // Random float with seed
seededBool, // Random boolean with seed
seededGrid, // Generate 2D grid with seed
getTodaysSeed, // Get seed for today's date
getSeedForDate, // Get seed for any date
parseSeedFromUrl, // Parse seed from URL params
setSeedInUrl, // Set seed in URL params
hashString, // Hash string to seed value
generateRandomSeed,
} from '@enigma';
Use createPackApi() for easy backend communication with automatic CSRF handling:
import { createPackApi } from '@enigma';
const api = createPackApi('my-pack-id');
// GET request
const data = await api.get('/leaderboard');
// POST request (handles CSRF automatically)
await api.post('/submit-score', { score: 100, time: 42 });
// Other methods
await api.put('/update', { ... });
await api.delete('/item/123');
Display the current puzzle seed with copy and share functionality:
import { SeedDisplay, useSeed, generateRandomSeed } from '@enigma';
function MyGame() {
const [seed, setSeed] = useState(() => getTodaysSeed('my-game'));
const handleNewPuzzle = () => {
setSeed(generateRandomSeed());
};
return (
<div>
{/* Default variant */}
<SeedDisplay
seed={seed}
showNewButton
onNewSeed={handleNewPuzzle}
/>
{/* Compact variant for tight spaces */}
<SeedDisplay seed={seed} variant="compact" />
{/* Inline variant for text flow */}
<SeedDisplay seed={seed} variant="inline" showShare={false} />
</div>
);
}
Props:
seed: The seed value (number or string)label: Label text (default: "Seed")variant: "default" | "compact" | "inline"showCopy: Show copy button (default: true)showShare: Show share button (default: true)showNewButton: Show new puzzle button (default: false)onNewSeed: Callback for new puzzle buttonGames should be reproducible given a seed number:
import { createSeededRNG, seededShuffle, getTodaysSeed } from '@enigma';
// Daily puzzle: same seed for everyone today
const seed = getTodaysSeed('my-game');
const rng = createSeededRNG(seed);
// Use rng for all randomness
const shuffled = seededShuffle(items, rng);
const randomNum = Math.floor(rng() * 100);
// Display seed so users can share specific puzzles
console.log(`Puzzle #${seed}`);
Enigma provides CSS custom properties that automatically adapt to light/dark themes. Use these variables in your game styles to ensure consistent theming.
/* Background colors */
--color-bg /* Main background (#050510 dark, #f8f7ff light) */
--color-bg-secondary /* Secondary panels */
--color-bg-tertiary /* Tertiary/subtle areas */
--color-bg-card /* Card backgrounds */
--color-bg-input /* Form inputs */
--color-bg-hover /* Hover states */
/* Text colors */
--color-text /* Primary text */
--color-text-secondary /* Secondary/muted text */
--color-text-tertiary /* Tertiary/subtle text */
--color-text-muted /* Very muted text */
/* Primary (accent) colors */
--color-primary /* Main brand color (purple) */
--color-primary-light /* Lighter variant */
--color-primary-dark /* Darker variant */
--color-primary-bg /* Subtle background tint */
--color-primary-bg-hover /* Hover state for primary bg */
--color-primary-border /* Primary border color */
--color-primary-border-hover/* Hover state for primary border */
/* Semantic colors */
--color-success /* Success states (green) */
--color-success-bg /* Success background */
--color-success-border /* Success border */
--color-danger /* Error/danger states (red) */
--color-danger-bg /* Danger background */
--color-danger-border /* Danger border */
--color-warning /* Warning states (yellow) */
--color-warning-bg /* Warning background */
/* Borders */
--color-border /* Standard border */
--color-border-light /* Subtle border */
/* Panels and containers */
--game-panel-bg /* Game panel backgrounds */
--game-panel-border /* Game panel borders */
/* Keyboard/input keys */
--game-key-bg /* Key background */
--game-key-border /* Key border */
--game-key-text /* Key text color */
--game-key-hover-bg /* Key hover state */
/* Grid cells */
--game-cell-bg /* Cell background */
--game-cell-border /* Cell border */
--game-cell-text /* Cell text color */
--game-cell-hover /* Cell hover state */
/* Accent colors for game states */
--game-accent-red /* Error/wrong states */
--game-accent-red-dark
--game-accent-red-bg
--game-accent-red-border
--game-accent-green /* Correct/success states */
--game-accent-green-dark
--game-accent-green-bg
--game-accent-green-border
--game-accent-blue /* Selected/highlighted states */
--game-accent-blue-dark
--game-accent-blue-bg
--game-accent-blue-border
--game-accent-yellow /* Hint/warning states */
--game-accent-yellow-dark
--game-accent-yellow-bg
--game-accent-yellow-border
--game-accent-purple /* Special states */
--game-accent-purple-light
--game-accent-purple-bg
--game-accent-purple-border
/* Misc game elements */
--game-gallows /* Hangman gallows color */
--game-slot-blank /* Blank slot color */
--btn-secondary-bg /* Secondary button background */
--btn-secondary-border /* Secondary button border */
--btn-secondary-text /* Secondary button text */
--btn-secondary-hover-bg /* Secondary button hover */
--panel-bg /* Generic panel background */
--panel-border /* Generic panel border */
--shadow-glow /* Glowing shadow effect */
--shadow-card /* Card drop shadow */
--gradient-bg-1 /* Background gradient color 1 */
--gradient-bg-2 /* Background gradient color 2 */
--gradient-bg-3 /* Background gradient color 3 */
/* MyGame.module.css */
.container {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
color: var(--color-text);
}
.cell {
background: var(--game-cell-bg);
border: 1px solid var(--game-cell-border);
color: var(--game-cell-text);
transition: all 0.2s ease;
}
.cell:hover {
background: var(--game-cell-hover);
}
.cell.correct {
background: var(--game-accent-green-bg);
border-color: var(--game-accent-green-border);
color: var(--game-accent-green);
}
.cell.wrong {
background: var(--game-accent-red-bg);
border-color: var(--game-accent-red-border);
}
.button {
background: var(--color-primary-bg);
border: 1px solid var(--color-primary-border);
color: var(--color-primary);
}
.button:hover {
background: var(--color-primary-bg-hover);
border-color: var(--color-primary-border-hover);
}
The current theme is set via data-theme attribute on the root element:
[data-theme="dark"] or no attribute (default)[data-theme="light"]// Check current theme in JavaScript
const isDark = document.documentElement.dataset.theme !== 'light';
Create a new GitHub repository with this structure:
my-puzzle-pack/
├── manifest.js # Required: Pack metadata
├── games/ # Your game components
│ └── MyGame/
│ ├── index.jsx
│ └── MyGame.module.css
├── components/ # Shared components (optional)
└── backend/ # Backend plugin (optional)
└── plugin.js
Create a manifest.js with your pack info (see Manifest Reference)
Tag a release with a semantic version:
git tag v1.0.0
git push origin v1.0.0
Share your repository URL with users!
Enigma uses semantic versioning to manage pack versions:
v1.0.0, 1.0.0, v2.1.0-beta, etc.main/master)my-pack/
├── manifest.js # Pack metadata and game registry
├── README.md # Documentation (optional but recommended)
│
├── games/ # Game components
│ ├── GameOne/
│ │ ├── index.jsx # Main component (lazy-loaded)
│ │ └── GameOne.module.css
│ └── GameTwo/
│ └── index.jsx
│
├── components/ # Shared UI components
│ └── SharedComponent/
│ ├── index.js
│ ├── SharedComponent.jsx
│ └── SharedComponent.module.css
│
├── backend/ # Backend plugin (optional)
│ └── plugin.js # API routes and database migrations
│
└── assets/ # Static assets (optional)
└── images/
| File | Required | Purpose |
|---|---|---|
manifest.js |
✅ Yes | Pack metadata, game definitions, configuration |
backend/plugin.js |
❌ No | Server-side API routes and database |
games/*/index.jsx |
✅ Yes | React components for each game |
README.md |
❌ No | Documentation for your pack |
The manifest.js file defines your pack's metadata and games:
const myPack = {
// Required fields
id: 'my-pack-id', // Unique identifier (alphanumeric, hyphens)
name: 'My Puzzle Pack', // Display name
description: 'A collection of fun puzzles',
// Optional metadata
type: 'community', // 'official' | 'community' | 'custom'
version: '1.0.0', // Semantic version
author: 'Your Name',
icon: '🧩', // Emoji or path to icon
color: '#8b5cf6', // Theme color (hex)
// Pack behavior
default: false, // Auto-install? (usually false for community)
removable: true, // Can users uninstall?
hasBackend: false, // Has backend/plugin.js?
// Game definitions
categories: [
{
name: 'Category Name',
icon: '🎮',
description: 'Games in this category',
games: [
{
slug: 'my-game', // URL-safe identifier
title: 'My Game', // Display title
description: 'Game description',
icon: '🎯',
emojiIcon: '🎯',
colors: {
primary: '#8b5cf6',
secondary: '#7c3aed'
},
gradient: 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)',
component: () => import('./games/MyGame'), // Lazy load
},
],
},
],
// Helper methods (recommended)
get allGames() {
return this.categories.flatMap(cat =>
cat.games.map(game => ({ ...game, categoryName: cat.name }))
);
},
getGameBySlug(slug) {
return this.allGames.find(g => g.slug === slug);
},
get gameCount() {
return this.allGames.length;
},
getPreviewGames() {
return this.allGames.slice(0, 4);
},
};
export default myPack;
If your pack needs server-side functionality (APIs, databases, authentication), create a backend/plugin.js:
export default {
name: 'my-pack-id', // Must match manifest.id
version: '1.0.0',
// Database migrations (run automatically)
migrations: [
{
version: 1,
up: `
CREATE TABLE IF NOT EXISTS my_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
data TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
`
},
{
version: 2,
up: `ALTER TABLE my_table ADD COLUMN extra TEXT;`
}
],
// Register API routes
register(router, context) {
// Public endpoint
router.get('/data', (req, res) => {
const rows = context.db.all('SELECT * FROM my_table');
res.json({ data: rows });
});
// Authenticated endpoint
router.post('/save', context.requireAuth, (req, res) => {
const user = context.getCurrentUser(req);
context.db.run(
'INSERT INTO my_table (user_id, data) VALUES (?, ?)',
[user.id, req.body.data]
);
res.json({ success: true });
});
}
};
Routes are mounted at /api/packs/{pack-id}/:
GET /api/packs/my-pack-id/dataPOST /api/packs/my-pack-id/saveThe context parameter provides:
| Property | Description |
|---|---|
context.db |
Plugin's isolated SQLite database |
context.db.run(sql, params) |
Execute INSERT/UPDATE/DELETE |
context.db.get(sql, params) |
Get single row |
context.db.all(sql, params) |
Get all rows |
context.db.exec(sql) |
Execute raw SQL |
context.requireAuth |
Middleware requiring authentication |
context.getCurrentUser(req) |
Get { id } of logged-in user |
context.core.getUser(id) |
Get user info (read-only) |
context.core.getUsers(ids) |
Get multiple users |
context.core.getUsernameMap(ids) |
Map of user IDs to usernames |
🔒 Security: Each plugin has its own isolated SQLite database. Plugins cannot access:
Read-only access to user info is available through context.core.* APIs.
Use the createPackApi helper from the SDK (recommended):
import { createPackApi } from '@enigma';
const api = createPackApi('my-pack-id');
// GET request
const data = await api.get('/data');
// POST request (handles CSRF automatically)
await api.post('/save', { data: 'value' });
Or manually:
// In your game component
const API_URL = import.meta.env.VITE_API_URL || '';
async function fetchData() {
const response = await fetch(`${API_URL}/api/packs/my-pack-id/data`, {
credentials: 'include',
});
return response.json();
}
async function saveData(data) {
// Get CSRF token first
const csrfRes = await fetch(`${API_URL}/api/csrf-token`, { credentials: 'include' });
const { csrfToken } = await csrfRes.json();
const response = await fetch(`${API_URL}/api/packs/my-pack-id/save`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify({ data }),
});
return response.json();
}
Community packs with backends run server-side code on your Enigma installation:
context.requireAuth for user-specific endpointsEnigma applies these protections to all plugins:
The Docker container needs git installed. Ensure your docker-compose.yml includes:
command: >
sh -c "apk add --no-cache git && npm install && npm start"
Then recreate the container: docker-compose up -d --force-recreate enigma-backend
Ensure your repository has a manifest.js file in the root directory (not in a subdirectory).
Create a semantic version tag:
git tag v1.0.0
git push origin v1.0.0
docker-compose logs enigma-backendPOST /api/packs/plugins/reloadbackend/plugin.js exists and exports correctlyname in plugin matches id in manifestSee the EnigmaSampleCommunityPack repository for a complete working example with:
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/community-sources |
List all sources |
| POST | /api/community-sources |
Add a new source |
| DELETE | /api/community-sources/:id |
Remove a source |
| POST | /api/community-sources/:id/install |
Install pack |
| POST | /api/community-sources/:id/uninstall |
Uninstall pack |
| POST | /api/community-sources/:id/update |
Update to new version |
| POST | /api/community-sources/:id/check-update |
Check for updates |
| POST | /api/community-sources/check-all-updates |
Check all sources |
| GET | /api/community-sources/git-status |
Check git availability |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/packs/installed |
List installed packs |
| POST | /api/packs/install |
Install a pack |
| POST | /api/packs/uninstall |
Uninstall a pack |
| GET | /api/packs/plugins/status |
Get loaded plugins |
| POST | /api/packs/plugins/reload |
Reload all plugins |