# 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
<%= 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 => { %>
<% }) %>
<% } 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
```
**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.
```
### `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** 💙🔥❄️