# Trinity Console 2.0 — Complete Implementation Guide **Version:** 1.0 **Created:** April 5, 2026 **Authors:** Michael (The Wizard), Claude (Chronicler #61), Gemini (Architectural Partner) **Purpose:** Cold-start handoff document — any AI or developer can implement Trinity Console 2.0 from this single document. --- ## Table of Contents 1. [Project Overview](#1-project-overview) 2. [Architecture Decisions](#2-architecture-decisions) 3. [Complete File Structure](#3-complete-file-structure) 4. [Database Migrations](#4-database-migrations) 5. [Core Engine Code](#5-core-engine-code) 6. [RBAC System Code](#6-rbac-system-code) 7. [Feature Flags Code](#7-feature-flags-code) 8. [Authentication System](#8-authentication-system) 9. [Branding & Design System](#9-branding--design-system) 10. [Example Module (Dashboard)](#10-example-module-dashboard) 11. [Deployment Infrastructure](#11-deployment-infrastructure) 12. [Implementation Checklist](#12-implementation-checklist) 13. [Migration from Arbiter 3.5](#13-migration-from-arbiter-35) --- ## 1. Project Overview ### What is Trinity Console 2.0? Trinity Console is the admin panel for Firefrost Gaming, a subscription-based modded Minecraft server community. Version 2.0 is a complete architectural rewrite from a monolithic Express app to a modular plugin-based system. ### The Trinity (Users) | Name | Role | Username | Default Permission Set | |------|------|----------|------------------------| | Michael | The Wizard (Owner) | Frostystyle | Admin (full access) | | Meg | The Emissary (Community) | Gingerfury | Community (player management) | | Holly | The Catalyst (Builder) | unicorn20089 | Builder (server/infrastructure) | ### Key Characteristics - **Stack:** Node.js 20, Express, HTMX, Tailwind CSS, PostgreSQL, EJS - **Auth:** Discord OAuth (only Trinity staff can access) - **Design:** Dark theme with Firefrost branding (Fire/Frost/Arcane colors) - **Deployment:** Hybrid (Dev VPS for development, Command Center for production) ### Brand Colors | Name | Hex | CSS Variable | Usage | |------|-----|--------------|-------| | Fire | `#FF6B35` | `fire` | Fire path, energy, CTAs | | Frost | `#4ECDC4` | `frost` | Frost path, calm, info | | Arcane | `#A855F7` | `arcane` | Trinity/founder elements | | Gold | `#FFD700` | `gold` | Highlights, achievements | | Void (Dark) | `#0F0F1E` | `void` | Backgrounds | | Void Surface | `#16162C` | `void-surface` | Cards, panels | | Void Hover | `#1D1D3A` | `void-hover` | Hover states | | Void Border | `#2A2A4A` | `void-border` | Dividers | --- ## 2. Architecture Decisions ### Module System - **12 consolidated modules** (not 35 thin ones) - **Discovery:** `fs.readdirSync` on startup scans `src/modules/` - **Load order:** Topological sort based on dependencies, fatal on circular deps - **Hot reload:** Skip — nodemon restart is acceptable - **Communication:** Shared DB pool + EventEmitter (`core.events.emit`) ### Modules List | # | Module ID | Name | Contains | |---|-----------|------|----------| | 1 | dashboard | Dashboard | Overview, stats, quick actions | | 2 | players | Players | Subscribers, bans, support, community notes, grace period, role audit | | 3 | servers | Servers & Scheduling | Game servers, scheduler, whitelist sync | | 4 | infrastructure | Infrastructure | Server inventory, services, domains, backups | | 5 | financials | Financials | MRR/ARR, transactions, projections, expenses | | 6 | tasks | Tasks | Work tracking, assignments, blockers, comments | | 7 | docs | Docs | Ops manual editing via Gitea API, handoffs | | 8 | team | Team | Staff roster, availability, calendar | | 9 | marketing | Marketing | Social embeds/webhooks, announcements, assets | | 10 | chroniclers | Chroniclers | Lineage tracking, memorials, portraits, Codex status | | 11 | system | System | Modules mgmt, permissions, users, audit log, settings, feature flags, About page | | 12 | health | Health | Uptime Kuma webhook integration, deadman's switch alerts | ### RBAC (Role-Based Access Control) - **Model:** Roles + Direct Overrides (Option C) - **Resolution:** Overrides → Role → Default Deny - **Wildcards:** Store `tasks.*` as literal, resolve in middleware ### Versioning - **Platform:** Trinity Console 2.0 - **Migrated modules:** Start at 1.0.0 - **New modules:** Start at 0.1.0 until stable ### Deployment Strategy (Strangler Fig) 1. Develop on Dev VPS (64.50.188.128) 2. Connect to Command Center's PostgreSQL (63.143.34.217) 3. Migrate one module at a time 4. Old routes coexist with new until verified 5. Promote to Command Center when stable 6. Dev VPS becomes permanent staging --- ## 3. Complete File Structure ``` /opt/trinity-console/ ├── ecosystem.config.js # PM2 configuration ├── package.json ├── .env # Environment variables ├── tailwind.config.js # Tailwind with Firefrost colors │ ├── migrations/ # Core database migrations (run manually first) │ ├── 001_rbac_tables.sql │ ├── 002_feature_flags.sql │ └── 003_auth_users.sql │ ├── public/ │ └── css/ │ └── output.css # Compiled Tailwind │ ├── views/ │ └── layout.ejs # Master layout with sidebar │ └── src/ ├── index.js # Main entry point ├── db.js # PostgreSQL pool │ ├── core/ │ ├── boot.js # System initialization orchestrator │ │ │ ├── auth/ │ │ ├── routes.js # Login, logout, profile routes │ │ ├── strategy.js # Discord OAuth strategy │ │ ├── middleware.js # requireActiveUser (gatekeeper) │ │ ├── webhook.js # Discord webhook for pending users │ │ └── views/ │ │ ├── login.ejs │ │ ├── profile.ejs │ │ ├── pending.ejs │ │ └── rejected.ejs │ │ │ ├── database/ │ │ └── migrations.js # Per-module migration runner │ │ │ ├── events/ │ │ └── index.js # EventEmitter registry │ │ │ ├── features/ │ │ ├── index.js # Feature flag cache │ │ ├── middleware.js # requireFeature() │ │ └── routes.js # Toggle API │ │ │ ├── modules/ │ │ ├── loader.js # fs.readdirSync + topological sort │ │ ├── registry.js # In-memory module store │ │ └── routes.js # Dynamic route mounting │ │ │ ├── navigation/ │ │ └── index.js # Permission-filtered sidebar builder │ │ │ └── permissions/ │ ├── middleware.js # requirePermission() │ ├── resolver.js # fetchUserPermissions() + hasPermission() │ ├── routes.js # RBAC API endpoints │ └── sync.js # Upsert permissions from module.json │ └── modules/ ├── dashboard/ │ ├── module.json │ ├── routes.js │ ├── api.js │ ├── events.js │ ├── views/ │ │ └── index.ejs │ └── migrations/ │ └── 001_dashboard_stats.sql │ ├── system/ │ ├── module.json │ ├── routes.js │ └── views/ │ ├── index.ejs │ ├── users.ejs │ ├── permissions.ejs │ └── about.ejs │ ├── servers/ │ ├── module.json │ ├── routes.js │ └── views/ │ ├── index.ejs │ └── scheduler.ejs │ └── [other modules follow same pattern] ``` --- ## 4. Database Migrations **CRITICAL:** Run these manually via psql BEFORE first boot to avoid migration failures. ### `migrations/001_rbac_tables.sql` ```sql -- 1. Roles Table CREATE TABLE IF NOT EXISTS roles ( id SERIAL PRIMARY KEY, name VARCHAR(50) UNIQUE NOT NULL, description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 2. Permissions Table CREATE TABLE IF NOT EXISTS permissions ( permission_key VARCHAR(100) PRIMARY KEY, module_id VARCHAR(50) NOT NULL, name VARCHAR(100) NOT NULL, description TEXT, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 3. Role Permissions Table CREATE TABLE IF NOT EXISTS role_permissions ( role_id INT REFERENCES roles(id) ON DELETE CASCADE, permission_key VARCHAR(100) REFERENCES permissions(permission_key) ON DELETE CASCADE, PRIMARY KEY (role_id, permission_key) ); -- 4. User Roles Table CREATE TABLE IF NOT EXISTS user_roles ( user_id VARCHAR(50) PRIMARY KEY, role_id INT REFERENCES roles(id) ON DELETE CASCADE, assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, assigned_by VARCHAR(50) ); -- 5. User Permission Overrides Table CREATE TABLE IF NOT EXISTS user_permission_overrides ( user_id VARCHAR(50) NOT NULL, permission_key VARCHAR(100) REFERENCES permissions(permission_key) ON DELETE CASCADE, is_granted BOOLEAN NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by VARCHAR(50), PRIMARY KEY (user_id, permission_key) ); -- 6. Core System Migrations Table CREATE TABLE IF NOT EXISTS core_migrations ( id SERIAL PRIMARY KEY, module_id VARCHAR(50) NOT NULL, migration_file VARCHAR(255) NOT NULL, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(module_id, migration_file) ); -- Indexes CREATE INDEX IF NOT EXISTS idx_user_overrides ON user_permission_overrides(user_id); CREATE INDEX IF NOT EXISTS idx_role_permissions ON role_permissions(role_id); CREATE INDEX IF NOT EXISTS idx_active_permissions ON permissions(is_active); -- Seed Roles INSERT INTO roles (name, description) VALUES ('Admin', 'Full system access (The Wizard)'), ('Community', 'Player management and support (The Emissary)'), ('Builder', 'Server and infrastructure management (The Catalyst)') ON CONFLICT (name) DO NOTHING; ``` ### `migrations/002_feature_flags.sql` ```sql CREATE TABLE IF NOT EXISTS feature_flags ( key VARCHAR(100) PRIMARY KEY, is_enabled BOOLEAN NOT NULL DEFAULT FALSE, description TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_by VARCHAR(50) ); ``` ### `migrations/003_auth_users.sql` ```sql -- 1. Create the Users table CREATE TABLE IF NOT EXISTS users ( discord_id VARCHAR(50) PRIMARY KEY, username VARCHAR(100) NOT NULL, avatar VARCHAR(255), status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'approved', 'rejected' last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 2. Seed the Founders (Replace with actual Discord IDs!) INSERT INTO users (discord_id, username, status) VALUES ('MICHAELS_DISCORD_ID', 'Frostystyle', 'approved'), ('MEGS_DISCORD_ID', 'Gingerfury', 'approved'), ('HOLLYS_DISCORD_ID', 'unicorn20089', 'approved') ON CONFLICT (discord_id) DO UPDATE SET status = 'approved'; -- 3. Auto-assign roles to founders INSERT INTO user_roles (user_id, role_id, assigned_by) SELECT 'MICHAELS_DISCORD_ID', id, 'SYSTEM' FROM roles WHERE name = 'Admin' ON CONFLICT (user_id) DO NOTHING; INSERT INTO user_roles (user_id, role_id, assigned_by) SELECT 'MEGS_DISCORD_ID', id, 'SYSTEM' FROM roles WHERE name = 'Community' ON CONFLICT (user_id) DO NOTHING; INSERT INTO user_roles (user_id, role_id, assigned_by) SELECT 'HOLLYS_DISCORD_ID', id, 'SYSTEM' FROM roles WHERE name = 'Builder' ON CONFLICT (user_id) DO NOTHING; ``` --- ## 5. Core Engine Code ### `src/core/boot.js` ```javascript const path = require('path'); const { loadModules } = require('./modules/loader'); const registry = require('./modules/registry'); const { runModuleMigrations } = require('./database/migrations'); const { mountModuleRoutes } = require('./modules/routes'); const { loadModuleEvents } = require('./events'); const { syncPermissions } = require('./permissions/sync'); const { refreshCache: refreshFeatureCache } = require('./features'); async function bootSystem(app, pool) { console.log('[Boot] Initializing Trinity Console 2.0...'); // 1. Load Modules const modulesPath = path.join(__dirname, '../modules'); const modules = loadModules(modulesPath); console.log(`[Boot] Discovered ${modules.length} modules`); // 2. Register, Migrate, Mount for (const mod of modules) { registry.register(mod); await runModuleMigrations(pool, mod); mountModuleRoutes(app, mod); loadModuleEvents(mod); await syncPermissions(pool, mod); } // 3. Load Feature Flags await refreshFeatureCache(pool); console.log('[Boot] Trinity Console 2.0 ready.'); } module.exports = { bootSystem }; ``` ### `src/core/modules/registry.js` ```javascript const modules = new Map(); module.exports = { register: (moduleConfig) => { modules.set(moduleConfig.id, moduleConfig); }, getModule: (id) => modules.get(id), getAllModules: () => Array.from(modules.values()), isModuleEnabled: (id) => modules.has(id) }; ``` ### `src/core/modules/loader.js` ```javascript const fs = require('fs'); const path = require('path'); function loadModules(modulesPath) { if (!fs.existsSync(modulesPath)) return []; const folders = fs.readdirSync(modulesPath, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) .map(dirent => dirent.name); const loadedModules = []; for (const folder of folders) { const configPath = path.join(modulesPath, folder, 'module.json'); if (fs.existsSync(configPath)) { try { const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); config.dirPath = path.join(modulesPath, folder); loadedModules.push(config); } catch (err) { console.error(`[Core] Failed to parse module.json for ${folder}:`, err.message); } } } return topologicalSort(loadedModules); } function topologicalSort(modules) { const sorted = []; const visited = new Set(); const visiting = new Set(); const moduleMap = new Map(modules.map(m => [m.id, m])); function visit(moduleId) { if (visiting.has(moduleId)) { throw new Error(`[Core] Fatal: Circular dependency detected involving module '${moduleId}'`); } if (visited.has(moduleId)) return; visiting.add(moduleId); const mod = moduleMap.get(moduleId); if (mod && mod.dependencies) { for (const dep of Object.keys(mod.dependencies)) { if (!moduleMap.has(dep)) { console.warn(`[Core] Warning: Module '${moduleId}' depends on missing module '${dep}'`); continue; } visit(dep); } } visiting.delete(moduleId); visited.add(moduleId); if (mod) sorted.push(mod); } for (const mod of modules) { visit(mod.id); } return sorted; } module.exports = { loadModules }; ``` ### `src/core/modules/routes.js` ```javascript const path = require('path'); const fs = require('fs'); function mountModuleRoutes(app, moduleConfig) { const routesPath = path.join(moduleConfig.dirPath, 'routes.js'); if (fs.existsSync(routesPath)) { const router = require(routesPath); const prefix = moduleConfig.routes || `/${moduleConfig.id}`; app.use(prefix, router); console.log(`[Routes] Mounted ${moduleConfig.id} at ${prefix}`); } } module.exports = { mountModuleRoutes }; ``` ### `src/core/database/migrations.js` ```javascript const fs = require('fs'); const path = require('path'); async function runModuleMigrations(pool, moduleConfig) { const migrationsDir = path.join(moduleConfig.dirPath, 'migrations'); if (!fs.existsSync(migrationsDir)) return; const files = fs.readdirSync(migrationsDir) .filter(f => f.endsWith('.sql')) .sort(); for (const file of files) { const checkRes = await pool.query( 'SELECT 1 FROM core_migrations WHERE module_id = $1 AND migration_file = $2', [moduleConfig.id, file] ); if (checkRes.rowCount === 0) { console.log(`[Migrations] Applying ${moduleConfig.id}/${file}...`); const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf8'); const client = await pool.connect(); try { await client.query('BEGIN'); await client.query(sql); await client.query( 'INSERT INTO core_migrations (module_id, migration_file) VALUES ($1, $2)', [moduleConfig.id, file] ); await client.query('COMMIT'); } catch (err) { await client.query('ROLLBACK'); throw new Error(`Migration failed: ${moduleConfig.id}/${file} - ${err.message}`); } finally { client.release(); } } } } module.exports = { runModuleMigrations }; ``` ### `src/core/events/index.js` ```javascript const EventEmitter = require('events'); const fs = require('fs'); const path = require('path'); class CoreEvents extends EventEmitter {} const coreEmitter = new CoreEvents(); function loadModuleEvents(moduleConfig) { const eventsPath = path.join(moduleConfig.dirPath, 'events.js'); if (fs.existsSync(eventsPath)) { const registerEvents = require(eventsPath); registerEvents(coreEmitter); console.log(`[Events] Registered events for ${moduleConfig.id}`); } } module.exports = { emitter: coreEmitter, loadModuleEvents }; ``` ### `src/core/navigation/index.js` ```javascript const registry = require('../modules/registry'); function buildNavigation(userPermissions) { const modules = registry.getAllModules(); const navStructure = {}; modules.forEach(mod => { if (!mod.nav) return; // Check if user has at least one permission for this module const modulePermKeys = (mod.permissions || []).map(p => p.key); const hasAccess = modulePermKeys.length === 0 || modulePermKeys.some(key => userPermissions.includes(key) || userPermissions.includes(`${mod.id}.*`) ); if (!hasAccess) return; const section = mod.nav.section || 'General'; if (!navStructure[section]) { navStructure[section] = []; } navStructure[section].push({ id: mod.id, name: mod.name, icon: mod.icon || 'circle', path: mod.routes || `/${mod.id}`, position: mod.nav.position || 99, badge: mod.nav.badge || null }); }); // Sort each section by position Object.keys(navStructure).forEach(section => { navStructure[section].sort((a, b) => a.position - b.position); }); return navStructure; } module.exports = { buildNavigation }; ``` --- ## 6. RBAC System Code ### `src/core/permissions/resolver.js` ```javascript async function fetchUserPermissions(pool, userId) { // 1. Get role-based permissions const roleRes = await pool.query(` SELECT p.permission_key FROM user_roles ur JOIN role_permissions rp ON ur.role_id = rp.role_id JOIN permissions p ON rp.permission_key = p.permission_key WHERE ur.user_id = $1 AND p.is_active = TRUE `, [userId]); const rolePermissions = roleRes.rows.map(r => r.permission_key); // 2. Get direct overrides const overrideRes = await pool.query(` SELECT permission_key, is_granted FROM user_permission_overrides WHERE user_id = $1 `, [userId]); const overrides = {}; overrideRes.rows.forEach(r => { overrides[r.permission_key] = r.is_granted; }); // 3. Apply overrides to role permissions const finalPermissions = new Set(rolePermissions); Object.entries(overrides).forEach(([key, granted]) => { if (granted) { finalPermissions.add(key); } else { finalPermissions.delete(key); } }); return Array.from(finalPermissions); } function hasPermission(userPermissions, requiredKey) { if (userPermissions.includes(requiredKey)) return true; // Check for wildcard (e.g., tasks.* grants tasks.edit) const modulePart = requiredKey.split('.')[0]; if (userPermissions.includes(`${modulePart}.*`)) return true; return false; } module.exports = { fetchUserPermissions, hasPermission }; ``` ### `src/core/permissions/middleware.js` ```javascript const { hasPermission } = require('./resolver'); function requirePermission(permissionKey) { return (req, res, next) => { if (!req.user || !req.userPermissions) { return res.redirect('/auth/login'); } if (hasPermission(req.userPermissions, permissionKey)) { return next(); } const isHtmx = req.headers['hx-request'] === 'true'; if (isHtmx) { return res.status(403).send(`
Access Denied: You do not have permission to access this resource.
`); } return res.status(403).render('errors/403', { message: `Permission required: ${permissionKey}` }); }; } module.exports = { requirePermission }; ``` ### `src/core/permissions/sync.js` ```javascript async function syncPermissions(pool, moduleConfig) { if (!moduleConfig.permissions || moduleConfig.permissions.length === 0) return; for (const perm of moduleConfig.permissions) { await pool.query(` INSERT INTO permissions (permission_key, module_id, name, description, is_active) VALUES ($1, $2, $3, $4, TRUE) ON CONFLICT (permission_key) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description, is_active = TRUE `, [perm.key, moduleConfig.id, perm.name, perm.description || '']); } console.log(`[Permissions] Synced ${moduleConfig.permissions.length} permissions for ${moduleConfig.id}`); } module.exports = { syncPermissions }; ``` ### `src/core/permissions/routes.js` ```javascript const express = require('express'); const router = express.Router(); const db = require('../../db'); const { requirePermission } = require('./middleware'); // All routes require system.permissions.manage router.use(requirePermission('system.permissions.manage')); // Get all roles router.get('/api/roles', async (req, res) => { const { rows } = await db.query('SELECT * FROM roles ORDER BY id'); res.json(rows); }); // Get permissions for a role router.get('/api/roles/:id/permissions', async (req, res) => { const { rows } = await db.query( 'SELECT permission_key FROM role_permissions WHERE role_id = $1', [req.params.id] ); res.json(rows.map(r => r.permission_key)); }); // Assign permission to role router.post('/api/roles/:id/permissions', async (req, res) => { const { permission_key } = req.body; await db.query( 'INSERT INTO role_permissions (role_id, permission_key) VALUES ($1, $2) ON CONFLICT DO NOTHING', [req.params.id, permission_key] ); res.sendStatus(201); }); // Remove permission from role router.delete('/api/roles/:id/permissions/:key', async (req, res) => { await db.query( 'DELETE FROM role_permissions WHERE role_id = $1 AND permission_key = $2', [req.params.id, req.params.key] ); res.sendStatus(200); }); // Get all users with roles router.get('/api/users', async (req, res) => { const { rows } = await db.query(` SELECT u.discord_id, u.username, u.avatar, u.status, r.name as role_name FROM users u LEFT JOIN user_roles ur ON u.discord_id = ur.user_id LEFT JOIN roles r ON ur.role_id = r.id ORDER BY u.username `); res.json(rows); }); // Assign role to user router.post('/api/users/:id/role', async (req, res) => { const { role_id } = req.body; await db.query(` INSERT INTO user_roles (user_id, role_id, assigned_by) VALUES ($1, $2, $3) ON CONFLICT (user_id) DO UPDATE SET role_id = EXCLUDED.role_id, assigned_by = EXCLUDED.assigned_by `, [req.params.id, role_id, req.user.discord_id]); res.sendStatus(200); }); // Approve user router.post('/api/users/:id/approve', async (req, res) => { await db.query( 'UPDATE users SET status = $1 WHERE discord_id = $2', ['approved', req.params.id] ); res.sendStatus(200); }); // Get user overrides router.get('/api/users/:id/overrides', async (req, res) => { const { rows } = await db.query( 'SELECT permission_key, is_granted FROM user_permission_overrides WHERE user_id = $1', [req.params.id] ); res.json(rows); }); // Add user override router.post('/api/users/:id/overrides', async (req, res) => { const { permission_key, is_granted } = req.body; await db.query(` INSERT INTO user_permission_overrides (user_id, permission_key, is_granted, created_by) VALUES ($1, $2, $3, $4) ON CONFLICT (user_id, permission_key) DO UPDATE SET is_granted = EXCLUDED.is_granted `, [req.params.id, permission_key, is_granted, req.user.discord_id]); res.sendStatus(201); }); // Remove user override router.delete('/api/users/:id/overrides/:key', async (req, res) => { await db.query( 'DELETE FROM user_permission_overrides WHERE user_id = $1 AND permission_key = $2', [req.params.id, req.params.key] ); res.sendStatus(200); }); module.exports = router; ``` --- ## 7. Feature Flags Code ### `src/core/features/index.js` ```javascript let featureCache = {}; async function refreshCache(pool) { try { const { rows } = await pool.query('SELECT key, is_enabled FROM feature_flags'); const newCache = {}; rows.forEach(r => { newCache[r.key] = r.is_enabled; }); featureCache = newCache; console.log('[Features] Feature flag cache refreshed.'); } catch (err) { console.error('[Features] Failed to refresh cache:', err); } } function isEnabled(key) { return !!featureCache[key]; } async function setFlag(pool, key, enabled, updatedBy) { await pool.query(` INSERT INTO feature_flags (key, is_enabled, updated_by, updated_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP) ON CONFLICT (key) DO UPDATE SET is_enabled = EXCLUDED.is_enabled, updated_by = EXCLUDED.updated_by, updated_at = CURRENT_TIMESTAMP `, [key, enabled, updatedBy]); await refreshCache(pool); } module.exports = { refreshCache, isEnabled, setFlag }; ``` ### `src/core/features/middleware.js` ```javascript const { isEnabled } = require('./index'); function requireFeature(featureKey) { return (req, res, next) => { if (isEnabled(featureKey)) { return next(); } const isHtmx = req.headers['hx-request'] === 'true'; const msg = `Feature '${featureKey}' is currently disabled.`; if (isHtmx) { return res.status(503).send(`
Service Unavailable: ${msg}
`); } return res.status(503).render('errors/503', { message: msg }); }; } module.exports = { requireFeature }; ``` ### `src/core/features/routes.js` ```javascript const express = require('express'); const router = express.Router(); const db = require('../../db'); const { requirePermission } = require('../permissions/middleware'); const { setFlag } = require('./index'); router.use(requirePermission('system.features.manage')); router.get('/api/features', async (req, res) => { const { rows } = await db.query('SELECT * FROM feature_flags ORDER BY key'); res.json(rows); }); router.post('/api/features/:key', async (req, res) => { const { is_enabled } = req.body; await setFlag(db, req.params.key, is_enabled, req.user.discord_id); res.sendStatus(200); }); module.exports = router; ``` --- ## 8. Authentication System ### `src/core/auth/webhook.js` ```javascript async function notifyAdminOfPendingUser(user) { const webhookUrl = process.env.DISCORD_ADMIN_WEBHOOK_URL; if (!webhookUrl) return; const payload = { embeds: [{ title: "🔐 New Trinity Console Access Request", description: `**${user.username}** has logged in via Discord and is waiting for authorization.`, color: 16739381, // Fire orange thumbnail: { url: `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png` }, fields: [ { name: "Discord ID", value: `\`${user.id}\``, inline: true } ], url: `https://trinity.firefrostgaming.com/system/users` }] }; try { await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); } catch (err) { console.error('[Auth] Failed to send Discord webhook alert:', err); } } module.exports = { notifyAdminOfPendingUser }; ``` ### `src/core/auth/strategy.js` ```javascript const db = require('../../db'); const { notifyAdminOfPendingUser } = require('./webhook'); async function verifyDiscordLogin(accessToken, refreshToken, profile, done) { const { id, username, avatar } = profile; try { const result = await db.query(` INSERT INTO users (discord_id, username, avatar, last_login) VALUES ($1, $2, $3, CURRENT_TIMESTAMP) ON CONFLICT (discord_id) DO UPDATE SET username = EXCLUDED.username, avatar = EXCLUDED.avatar, last_login = CURRENT_TIMESTAMP RETURNING status, (xmax = 0) AS is_new_user; `, [id, username, avatar]); const dbUser = result.rows[0]; const sessionUser = { id: id, discord_id: id, username: username, avatar: avatar, status: dbUser.status }; if (dbUser.is_new_user && dbUser.status === 'pending') { await notifyAdminOfPendingUser(sessionUser); } return done(null, sessionUser); } catch (err) { console.error('[Auth Error]', err); return done(err, null); } } module.exports = { verifyDiscordLogin }; ``` ### `src/core/auth/middleware.js` ```javascript function requireActiveUser(req, res, next) { if (!req.user) { return res.redirect('/auth/login'); } if (req.user.status === 'pending') { if (req.headers['hx-request'] === 'true') { return res.status(403).send(`
Account pending authorization.
`); } return res.render('../src/core/auth/views/pending', { layout: false, user: req.user }); } if (req.user.status === 'rejected') { return res.render('../src/core/auth/views/rejected', { layout: false }); } next(); } module.exports = { requireActiveUser }; ``` ### `src/core/auth/routes.js` ```javascript const express = require('express'); const router = express.Router(); const db = require('../../db'); router.get('/auth/login', (req, res) => { if (req.user) return res.redirect('/dashboard'); res.render('../src/core/auth/views/login', { layout: false }); }); router.get('/auth/logout', (req, res) => { req.session.destroy((err) => { if (err) console.error('Session destruction error:', err); res.redirect('/auth/login?loggedOut=true'); }); }); router.get('/profile', async (req, res) => { if (!req.user) return res.redirect('/auth/login'); try { const roleRes = await db.query(` SELECT r.name FROM user_roles ur JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = $1`, [req.user.id] ); const roleName = roleRes.rowCount > 0 ? roleRes.rows[0].name : 'Member'; const { fetchUserPermissions } = require('../permissions/resolver'); const permissions = await fetchUserPermissions(db, req.user.id); res.render('../src/core/auth/views/profile', { user: req.user, roleName, permissions: permissions.sort() }); } catch (error) { console.error(error); res.status(500).send("Error loading profile"); } }); module.exports = router; ``` ### `src/core/auth/views/login.ejs` ```html Login | Trinity Console

Trinity Console

Command Center for Firefrost Gaming

Sign in with Discord
``` ### `src/core/auth/views/pending.ejs` ```html Access Pending | Trinity Console

Access Pending

Your Discord identity (<%= user.username %>) has been verified, but your clearance level has not yet been assigned by the Trinity.

STATUS: WAITING_FOR_WIZARD

An alert has been dispatched to the administrators. You will be able to access the console once your profile is approved.

Return to Login
``` ### `src/core/auth/views/profile.ejs` ```html
Avatar

<%= user.username %>

Connected via Discord

<%= roleName %>

Active Permissions

These are the specific clearance levels assigned to you by the Trinity.

<% if (permissions && permissions.length > 0) { %>
<% permissions.forEach(perm => { %>
<%= perm %>
<% }) %>
<% } else { %>

No explicit permissions assigned to this profile.

<% } %>
``` --- ## 9. Branding & Design System ### `tailwind.config.js` ```javascript /** @type {import('tailwindcss').Config} */ module.exports = { content: ["./src/**/*.{ejs,js,html}", "./views/**/*.ejs"], theme: { extend: { colors: { fire: { DEFAULT: '#FF6B35', hover: '#E55A2A', dim: 'rgba(255, 107, 53, 0.1)' }, frost: { DEFAULT: '#4ECDC4', hover: '#3EBAB1', dim: 'rgba(78, 205, 196, 0.1)' }, arcane: { DEFAULT: '#A855F7', hover: '#9333EA', dim: 'rgba(168, 85, 247, 0.1)' }, gold: { DEFAULT: '#FFD700', hover: '#E6C200', dim: 'rgba(255, 215, 0, 0.1)' }, void: { DEFAULT: '#0F0F1E', surface: '#16162C', hover: '#1D1D3A', border: '#2A2A4A' } }, fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] } }, }, plugins: [ require('@tailwindcss/forms'), ], } ``` ### `views/layout.ejs` ```html Trinity Console | Firefrost
<%- body %>
``` ### Component Reference **Cards:** ```html

Card Title

``` **Buttons:** ```html ``` **Form Inputs:** ```html
``` **Alerts:** ```html

Error message here.

Success message here.

``` **Badges:** ```html Online Admin Offline ``` --- ## 10. Example Module (Dashboard) ### `src/modules/dashboard/module.json` ```json { "id": "dashboard", "name": "Dashboard", "description": "System overview and quick stats", "version": "1.0.0", "author": "Trinity", "dependencies": {}, "icon": "home", "nav": { "section": "General", "position": 1 }, "permissions": [ { "key": "dashboard.view", "name": "View Dashboard", "description": "Access the main overview screen" } ], "routes": "/dashboard" } ``` ### `src/modules/dashboard/routes.js` ```javascript const express = require('express'); const router = express.Router(); const { requirePermission } = require('../../core/permissions/middleware'); router.use(requirePermission('dashboard.view')); router.get('/', (req, res) => { res.render('../src/modules/dashboard/views/index', { user: req.user }); }); module.exports = router; ``` ### `src/modules/dashboard/views/index.ejs` ```html

Welcome back, <%= user.username %>

The Frostwall holds strong.

Servers Online

12 / 12

Active Subscribers

47

Monthly Revenue

$385

``` ### `src/modules/dashboard/events.js` ```javascript module.exports = function(coreEvents) { coreEvents.on('server.status_change', (data) => { console.log(`[Dashboard] Server ${data.serverId} changed to ${data.status}`); }); }; ``` ### `src/modules/dashboard/api.js` ```javascript module.exports = { getSystemHealthScore: async () => { return 100; // Placeholder } }; ``` --- ## 11. Deployment Infrastructure ### Server Information | Server | IP | Role | |--------|-----|------| | Dev VPS | 64.50.188.128 | Development/Staging (Trinity 2.0 development) | | Command Center | 63.143.34.217 | Production (PostgreSQL, Arbiter 3.5) | ### Cloudflare DNS Create A Record: - **Name:** `trinity` - **Target:** `64.50.188.128` - **Proxy:** Gray Cloud for Certbot, then Orange Cloud ### PostgreSQL Remote Access (Command Center) **Step 1:** Edit `/etc/postgresql/*/main/postgresql.conf`: ``` listen_addresses = '*' ``` **Step 2:** Edit `/etc/postgresql/*/main/pg_hba.conf`: ``` host arbiter_db arbiter 64.50.188.128/32 scram-sha-256 ``` **Step 3:** Firewall: ```bash sudo ufw allow from 64.50.188.128 to any port 5432 ``` **Step 4:** Restart: ```bash sudo systemctl restart postgresql ``` ### Dev VPS Bootstrap ```bash # System updates sudo apt update && sudo apt upgrade -y # Install dependencies sudo apt install -y nginx certbot python3-certbot-nginx curl git ufw # Firewall sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full' sudo ufw enable # Node.js 20 curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs # PM2 sudo npm install -g pm2 ``` ### Nginx Configuration Create `/etc/nginx/sites-available/trinity`: ```nginx server { listen 80; server_name trinity.firefrostgaming.com; location / { proxy_pass http://localhost:3001; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; } } ``` Enable: ```bash sudo ln -s /etc/nginx/sites-available/trinity /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl restart nginx ``` ### SSL Certificate ```bash sudo certbot --nginx -d trinity.firefrostgaming.com ``` ### PM2 Configuration Create `ecosystem.config.js`: ```javascript module.exports = { apps: [{ name: "trinity-console", script: "./src/index.js", watch: false, max_memory_restart: "1G", env: { NODE_ENV: "production", PORT: 3001 } }] }; ``` ### Environment Variables (`.env`) ```env # Database (Command Center) DB_USER=arbiter DB_HOST=63.143.34.217 DB_NAME=arbiter_db DB_PASSWORD=FireFrost2026!Arbiter DB_PORT=5432 # Discord DISCORD_CLIENT_ID=1330262498058670162 DISCORD_CLIENT_SECRET=[from Vaultwarden] DISCORD_BOT_TOKEN=[from Vaultwarden] DISCORD_GUILD_ID=1286373938067198003 DISCORD_ADMIN_WEBHOOK_URL=[create webhook in admin channel] # Stripe STRIPE_SECRET_KEY=[from Vaultwarden] STRIPE_WEBHOOK_SECRET=[from Vaultwarden] # Session SESSION_SECRET=[from Vaultwarden] # Pterodactyl PTERO_CLIENT_KEY=ptlc_NDkYX6yPPBHZacPmViFWtl4AvopzgxNcnHoQTOOtQEl # App PORT=3001 NODE_ENV=production BASE_URL=https://trinity.firefrostgaming.com ``` --- ## 12. Implementation Checklist ### Phase 0: Infrastructure Setup - [ ] Create Cloudflare A record for `trinity` → 64.50.188.128 - [ ] Update Command Center PostgreSQL for remote access - [ ] Open UFW port 5432 for Dev VPS IP - [ ] Bootstrap Dev VPS (Node, PM2, Nginx, Certbot) - [ ] Run Certbot for SSL ### Phase 1: Core Foundation - [ ] Run 001_rbac_tables.sql manually on Command Center - [ ] Run 002_feature_flags.sql manually - [ ] Run 003_auth_users.sql manually (with real Discord IDs) - [ ] Create project structure on Dev VPS - [ ] Implement `src/db.js` - [ ] Implement `src/core/boot.js` - [ ] Implement `src/core/modules/` (registry, loader, routes) - [ ] Implement `src/core/events/index.js` - [ ] Implement `src/core/navigation/index.js` - [ ] Create `tailwind.config.js` and compile CSS - [ ] Create `views/layout.ejs` - [ ] Test boot sequence ### Phase 2: RBAC & Auth - [ ] Implement `src/core/permissions/` (resolver, middleware, sync, routes) - [ ] Implement `src/core/auth/` (strategy, middleware, webhook, routes, views) - [ ] Configure Passport Discord strategy - [ ] Test Discord OAuth flow - [ ] Test pending user flow - [ ] Test founder auto-approval ### Phase 3: First Modules - [ ] Create Dashboard module - [ ] Create System module (with About page) - [ ] Test navigation building - [ ] Test permission enforcement ### Phase 4: Migration (Strangler Fig) - [ ] Migrate Servers module (includes Scheduler) - [ ] Migrate Players module - [ ] Migrate Financials module - [ ] Build Tasks module (new) - [ ] Continue with remaining modules --- ## 13. Migration from Arbiter 3.5 ### Current Arbiter Structure ``` /opt/arbiter-3.0/src/ ├── routes/admin/ │ ├── dashboard.js │ ├── players.js │ ├── servers.js │ ├── scheduler.js │ └── financials.js └── views/admin/ ├── dashboard.ejs ├── players.ejs └── ... ``` ### Migration Steps Per Module 1. **Create module folder:** `src/modules/{name}/` 2. **Create module.json** with permissions 3. **Move routes:** Copy from `routes/admin/{name}.js` → `src/modules/{name}/routes.js` 4. **Update imports:** Change `../../views/admin/` → `./views/` 5. **Move views:** Copy from `views/admin/{name}.ejs` → `src/modules/{name}/views/` 6. **Add permission checks:** Wrap routes with `requirePermission()` 7. **Test in isolation** 8. **Remove old routes** once verified ### Servers Module Example **`src/modules/servers/module.json`:** ```json { "id": "servers", "name": "Servers & Scheduling", "description": "Game server status, whitelist sync, and restart automation", "version": "1.0.0", "author": "Trinity", "dependencies": {}, "icon": "server", "nav": { "section": "Infrastructure", "position": 1 }, "permissions": [ { "key": "servers.view", "name": "View Servers", "description": "See server matrix and status" }, { "key": "servers.manage", "name": "Manage Servers", "description": "Sync whitelists and toggle maintenance mode" }, { "key": "servers.scheduler", "name": "Manage Scheduler", "description": "Deploy global restart schedules and nuke conflicts" } ], "routes": "/servers" } ``` --- ## Emergency Contacts If Claude is unavailable: 1. **Gemini** — Knows the entire architecture (paste this doc for context) 2. **GPT-4o** — Can implement from this guide 3. **New Claude session** — Start fresh with this document --- ## Document History | Version | Date | Author | Changes | |---------|------|--------|---------| | 1.0 | 2026-04-05 | Claude (Chronicler #61) | Initial creation from 7 Gemini consultation rounds | --- **Fire + Frost + Foundation = Where Love Builds Legacy** 💙🔥❄️