xgit simple git

nexus

nexus

clone git clone https://kb.hax.al/nexus
PHP 8.0+ SQLite MySQL MIT License Zero Dependencies

███╗   ██╗███████╗██╗  ██╗██╗   ██╗███████╗
████╗  ██║██╔════╝╚██╗██╔╝██║   ██║██╔════╝
██╔██╗ ██║█████╗   ╚███╔╝ ██║   ██║███████╗
██║╚██╗██║██╔══╝   ██╔██╗ ██║   ██║╚════██║
██║ ╚████║███████╗██╔╝ ██╗╚██████╔╝███████║
╚═╝  ╚═══╝╚══════╝╚═╝  ╚═╝ ╚═════╝ ╚══════╝

No frameworks · No npm · No Docker · Just upload and run

Features · Quick Start · Installation · Configuration · Addons · API · FAQ


✨ Features

💬 Forum Core

  • Categories with icons, colours, sub-categories, and per-role permissions
  • Topics & threaded replies with pagination
  • Full Markdown editor — Google Docs-style toolbar with SVG icons, Write/Preview tabs
  • Syntax-highlighted code blocks via Prism.js (200+ languages, lazy-loaded only when needed)
  • Styled blockquotes with gradient left border
  • Post permalinks — every post gets #post-{id} + a 🔗 copy-link button (pagination-aware)
  • Inline image upload — paste, drag-drop, or file picker directly in the editor
  • Media auto-embeds — paste a URL and it becomes a player (14 platforms)
  • @mentions with live autocomplete
  • Live search — finds topics AND post content, links directly to the matching post on the correct page

🔐 Roles & Permissions

RoleLevelCan Do
Guest0Read public categories
Member10Post, reply, like, message, friend
Moderator20+ Pin/close topics, edit any post
Admin30Full access + admin panel

Per-category permissions — set independently for reading, posting, and replying:

PermissionOptions
Who can read🌐 Everyone · 👤 Members · 🛡️ Moderators+ · 👑 Admins
Who can post topicsSame four options
Who can replySame four options

⭐ Karma System

Eight progressive tiers earned through activity:

TierPointsIcon
Newcomer0–9🌱
Member10–49💬
Regular50–99
Contributor100–249🌟
Veteran250–499🔥
Expert500–999💎
Elite1000–2499👑
Legend2500+🏆

Admins can manually adjust karma (Add / Subtract / Set) with an optional reason that notifies the user.

📬 Private Messages

  • Inbox/Sent with unread badges
  • Conversation threads displayed as chat bubbles
  • Read receipts (✓ sent · ✓✓ read)
  • Online status indicator (green if active in last 5 min)
  • Live user search autocomplete
  • Topics tab — title matches
  • Posts tab — content matches, jumps directly to the exact post on the correct page
  • Users tab — username + bio search
  • Live header dropdown shows topic + post results simultaneously

🧩 Addon System

Extend the forum by dropping a folder into addons/ and clicking Activate. No core file edits needed. Full PHP API access with 9 event hooks.

🔒 Security

  • CSRF tokens on all forms and AJAX
  • bcrypt password hashing (cost 12)
  • Math captcha (admin toggle, separate for posts and new topics)
  • Rate limiting with live countdown
  • Auto-generated .htaccess protection for data/ and uploads/
  • Security headers: X-Content-Type-Options, X-Frame-Options, Referrer-Policy, HSTS on HTTPS
  • All SQL via PDO prepared statements

🗄️ Database Support

DriverVersion
SQLite3.x — zero configuration, single file
MySQL5.7+
MariaDB10.3+

Schema migrates automatically on every request — update files and existing installs upgrade themselves.


🚀 Quick Start

Shared Hosting (5 minutes)

# 1. Upload to your server
scp -r forum-clean/ user@host:~/public_html/forum/

# 2. Set directory permissions
chmod 755 data/ public/uploads/ public/uploads/avatars/

# 3. Visit the installer
# https://yoursite.com/forum/install/

# 4. Complete the 3-step wizard, then remove /install/
rm -rf install/

Local Development

# PHP built-in server — SQLite, zero config
cd forum-clean/
php -S localhost:8080
# open http://localhost:8080/install/

Docker (Apache)

# Dockerfile
FROM php:8.2-apache
RUN docker-php-ext-install pdo pdo_sqlite
RUN a2enmod rewrite
COPY forum-clean/ /var/www/html/
RUN chown -R www-data:www-data /var/www/html/data \
    /var/www/html/public/uploads
docker build -t nexus-forum .
docker run -p 8080:80 nexus-forum
# open http://localhost:8080/install/

📦 Installation

Requirements

ItemMinimumNotes
PHP8.08.2+ recommended
PDORequiredpdo_sqlite or pdo_mysql
GDOptionalFor image thumbnails
Web serverApache or NginxSee configs below
Disk10 MBPlus user uploads

Step-by-step

1 — Upload files

The forum works at any URL path:

  • https://yoursite.com/
  • https://yoursite.com/forum/
  • https://yoursite.com/community/board/

The BASE path is auto-detected. No .env changes needed.

2 — Set permissions

chmod 755 data/
chmod 755 public/uploads/
chmod 755 public/uploads/avatars/

3 — Run the web installer

Visit /install/ — the 3-step wizard:

StepWhat happens
1 — RequirementsChecks PHP version, extensions, directory permissions
2 — DatabaseChoose SQLite or MySQL, enter site name + admin credentials
3 — DoneWrites config, runs migration, shows security checklist

4 — Post-install (automatic)

The installer automatically creates:

  • data/.htaccess — denies all web access to the database directory
  • public/uploads/.htaccess — blocks PHP execution in uploads folder
  • data/db_config.phpchmod 0640
  • data/forum.dbchmod 0640 (SQLite only)
  • data/installed.lock — prevents re-running the installer

MySQL Setup

CREATE DATABASE nexus_forum
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

CREATE USER 'nexus'@'localhost' IDENTIFIED BY 'your_strong_password';
GRANT ALL PRIVILEGES ON nexus_forum.* TO 'nexus'@'localhost';
FLUSH PRIVILEGES;

Then select "MySQL / MariaDB" in the installer.


⚙️ Configuration

Apache

<VirtualHost *:80>
    ServerName forum.yoursite.com
    DocumentRoot /var/www/nexus-forum

    <Directory /var/www/nexus-forum>
        AllowOverride All
        Require all granted
    </Directory>

    # Protect database directory
    <Directory /var/www/nexus-forum/data>
        Require all denied
    </Directory>
</VirtualHost>

Nginx

server {
    listen 80;
    server_name forum.yoursite.com;
    root /var/www/nexus-forum;
    index index.php;

    # Block sensitive paths
    location ~ ^/(data|includes)/ {
        deny all;
        return 404;
    }
    location ~ \.(db|sqlite|lock)$ {
        deny all;
        return 404;
    }
    # Block PHP execution in uploads
    location ~ ^/public/uploads/.*\.php$ {
        deny all;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location / {
        try_files $uri $uri/ =404;
    }
}

Admin Settings Panel

Visit Admin → Settings to configure:

SettingDescription
Site name & descriptionHeader and ``</td></tr><tr><td>Topics / Posts per page</td><td>Pagination sizes</td></tr><tr><td>Post captcha</td><td>Math captcha on replies (spam protection)</td></tr><tr><td>Topic captcha</td><td>Math captcha on new topics</td></tr><tr><td>Rate limiting</td><td>Seconds between posts</td></tr><tr><td>Max upload size</td><td>Image upload limit</td></tr><tr><td>Registration</td><td>Open or closed</td></tr></tbody></table> <hr/> <h2 id="-project-structure">📁 Project Structure</h2> <pre><code>nexus-forum/ │ ├── 📁 addons/ # Drop addon folders here │ └── example-hello-world/ # Sample addon (see Addon docs) │ ├── 📁 admin/ # Admin panel │ ├── index.php # Dashboard │ ├── users.php # User list │ ├── user.php # Edit user + karma manager │ ├── categories.php # Categories + role permissions │ ├── topics.php # Topic moderation │ ├── settings.php # Site settings │ ├── themes.php # Theme switching │ └── addons.php # Addon manager + developer docs │ ├── 📁 api/ # JSON endpoints (POST) │ ├── reply.php # Post a reply │ ├── edit.php # Edit a post │ ├── delete.php # Delete a post │ ├── like.php # Like / unlike │ ├── upload.php # Image upload │ ├── notifications.php # Mark read │ ├── friend.php # Friend requests │ ├── karma.php # Admin karma adjust │ ├── chat.php # Private message actions │ ├── search.php # Live search (topics + posts) │ ├── search_users.php # User autocomplete │ └── topic_action.php # Pin / close / delete topic │ ├── 📁 auth/ # login · register · logout ├── 📁 data/ # Created by installer (not web-accessible) ├── 📁 forum/ # category · topic · new-topic · search │ ├── 📁 includes/ # Core library (not web-accessible) │ ├── bootstrap.php # Loads everything, boots addons │ ├── config.php # Path detection, security headers │ ├── db.php # PDO multi-driver DB class │ ├── functions.php # All helpers │ ├── markdown.php # Markdown + embed renderer │ └── addons.php # AddonManager class │ ├── 📁 install/ # DELETE after setup ├── 📁 messages/ # inbox · compose · view │ ├── 📁 public/ │ ├── css/main.css # ~2400 lines — full design system │ ├── js/app.js # ~1000 lines — all client JS │ └── uploads/ # User images (PHP execution blocked) │ ├── 📁 users/ # profile · edit · search │ ├── 📁 views/partials/ │ ├── layout.php # Header, sidebar, nav │ ├── layout_end.php # Footer, Prism.js loader, app.js │ ├── admin_layout.php # Admin sidebar │ └── editor_toolbar.php # Reusable Markdown toolbar (SVG icons) │ └── index.php # Homepage</code></pre> <hr/> <h2 id="-media-embeds">📺 Media Embeds</h2> <p>Paste any of these URLs alone on a line in a post and it auto-embeds as a player:</p> <table class="md-table"><thead><tr><th>Platform</th><th>Supported</th></tr></thead><tbody><tr><td>YouTube</td><td>Videos, Shorts, YouTube Music</td></tr><tr><td>Vimeo</td><td>Videos</td></tr><tr><td>Twitch</td><td>Live streams, VODs</td></tr><tr><td>Dailymotion</td><td>Videos</td></tr><tr><td>Streamable</td><td>Clips</td></tr><tr><td>Rumble</td><td>Videos</td></tr><tr><td>Spotify</td><td>Tracks, albums, playlists, podcast episodes, artist pages</td></tr><tr><td>SoundCloud</td><td>Tracks</td></tr><tr><td>Loom</td><td>Screen recordings</td></tr><tr><td>CodePen</td><td>Pens</td></tr><tr><td>JSFiddle</td><td>Fiddles</td></tr><tr><td>Twitter / X</td><td>Tweets</td></tr><tr><td>TED Talks</td><td>Talks</td></tr><tr><td>Bandcamp</td><td>Tracks</td></tr></tbody></table> <hr/> <h2 id="-addon-system">🧩 Addon System</h2> <h3 id="installing">Installing</h3> <ol><li>Drop the addon folder into <code>addons/</code></li><li><strong>Admin → Addons → ▶ Activate</strong></li></ol> <h3 id="creating-an-addon">Creating an Addon</h3> <p>**<code>nexus-addon.json</code>** — manifest (required)</p> <pre><code class="language-json">{ "name": "My Addon", "description": "What this addon does.", "version": "1.0.0", "author": "Your Name", "url": "https://yoursite.com", "hooks": ["after_topic_created", "render_post_footer"], "requires": { "nexus": ">=14" } }</code></pre> <p>**<code>main.php</code>** — entry point (required)</p> <pre><code class="language-php"><?php // Runs on every request when the addon is active // Inject HTML below every post addon_on('render_post_footer', function(array $post): string { return '<div class="my-badge">✓ Verified</div>'; }); // React to new topics addon_on('after_topic_created', function(array $data): void { // $data: topic_id, title, slug, category_id, user_id // Call external webhook, send Slack message, etc. // file_get_contents('https://hooks.example.com?title=' . urlencode($data['title'])); }); // Filter post HTML before display addon_on('render_post_content', function(string $html): string { return str_replace(':-)', '😊', $html); });</code></pre> <p>**<code>install.php</code>** — runs on activation (optional)</p> <pre><code class="language-php"><?php DB::connect()->exec("CREATE TABLE IF NOT EXISTS my_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, message TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP )"); cfg_set('my_addon_active', '1');</code></pre> <p>**<code>uninstall.php</code>** — runs on deactivation (optional)</p> <pre><code class="language-php"><?php cfg_set('my_addon_active', '0'); // DB::run("DROP TABLE IF EXISTS my_log"); // uncomment to clean up</code></pre> <h3 id="hook-reference">Hook Reference</h3> <table class="md-table"><thead><tr><th>Hook</th><th>Data passed</th><th>Return</th><th>Fires when</th></tr></thead><tbody><tr><td><code>after<em>topic</em>created</code></td><td><code>array</code> {topic<em>id, title, slug, category</em>id, user_id}</td><td>void</td><td>Topic saved</td></tr><tr><td><code>after<em>reply</em>saved</code></td><td><code>array</code> {post<em>id, topic</em>id, user_id}</td><td>void</td><td>Reply posted</td></tr><tr><td><code>after<em>user</em>registered</code></td><td><code>array</code> {user_id, username, email}</td><td>void</td><td>Registration</td></tr><tr><td><code>render<em>post</em>content</code></td><td><code>string</code> HTML</td><td><code>string</code> HTML</td><td>Before post output</td></tr><tr><td><code>render<em>post</em>footer</code></td><td><code>array</code> post row</td><td><code>string</code> HTML</td><td>Below post body</td></tr><tr><td><code>render<em>topic</em>header</code></td><td><code>array</code> topic row</td><td><code>string</code> HTML</td><td>Above topic</td></tr><tr><td><code>user<em>karma</em>changed</code></td><td><code>array</code> {user_id, old, new, by}</td><td>void</td><td>Karma adjusted</td></tr><tr><td><code>admin<em>nav</em>items</code></td><td><code>array</code> items</td><td><code>array</code></td><td>Admin sidebar</td></tr><tr><td><code>before<em>page</em>head</code></td><td><code>string</code> HTML</td><td><code>string</code> HTML</td><td>Inside `<head>`</td></tr></tbody></table> <h3 id="addon-php-api">Addon PHP API</h3> <pre><code class="language-php">// Database DB::rows("SELECT * FROM topics WHERE category_id=?", [$catId]); DB::row("SELECT * FROM users WHERE id=?", [$uid]); DB::insert("INSERT INTO my_log (message) VALUES (?)", [$msg]); DB::run("UPDATE my_table SET col=? WHERE id=?", [$val, $id]); DB::val("SELECT COUNT(*) FROM posts WHERE topic_id=?", [$tid]); // Current user global $USER; // array or null // Settings $val = cfg('site_name', 'My Forum'); cfg_set('my_key', 'my_value'); // Notifications add_notification($userId, 'my_type', ['key' => 'value']); // Karma add_karma($userId, 10); // add 10 points // URL helpers $url = u('forum/topic.php?slug=' . urlencode($slug)); $assetUrl = asset('js/app.js');</code></pre> <hr/> <h2 id="-theming">🎨 Theming</h2> <p>All design tokens are CSS custom properties in <code>public/css/main.css</code>:</p> <pre><code class="language-css">:root { /* Brand colours */ --blue: #3b82f6; --blue-d: #2563eb; --blue-l: #eff6ff; --green: #22c55e; --red: #ef4444; --purple: #8b5cf6; /* Surfaces */ --bg: #f1f5f9; /* page background */ --surface: #ffffff; /* cards */ --border: #e2e8f0; /* borders */ --border-l: #f1f5f9; /* light borders */ /* Text */ --text: #0f172a; --muted: #64748b; --faint: #94a3b8; /* Typography */ --font: 'Inter', -apple-system, sans-serif; --mono: 'JetBrains Mono', 'Fira Code', monospace; /* Sizing */ --r: 6px; /* border radius */ --r-lg: 10px; --r-xl: 16px; --header: 56px; --sidebar: 220px; }</code></pre> <p>Override any variable in a custom stylesheet, or inject one via the <code>before<em>page</em>head</code> addon hook.</p> <hr/> <h2 id="-api-reference">🔑 API Reference</h2> <p>All endpoints accept <code>POST</code> (or <code>GET</code> for search) and expect a <code>csrf</code> parameter from the <code>NX.csrf</code> global.</p> <table class="md-table"><thead><tr><th>Endpoint</th><th>Auth</th><th>Description</th></tr></thead><tbody><tr><td><code>POST /api/reply.php</code></td><td>Member</td><td>Post a reply (<code>slug</code>, <code>content</code>)</td></tr><tr><td><code>POST /api/edit.php</code></td><td>Author/Admin</td><td>Edit post (<code>post_id</code>, <code>content</code>)</td></tr><tr><td><code>POST /api/delete.php</code></td><td>Author/Admin</td><td>Delete post (<code>post_id</code>)</td></tr><tr><td><code>POST /api/like.php</code></td><td>Member</td><td>Like/unlike (<code>post_id</code>)</td></tr><tr><td><code>POST /api/upload.php</code></td><td>Member</td><td>Upload image (<code>file</code>) → <code>{url}</code></td></tr><tr><td><code>POST /api/topic_action.php</code></td><td>Mod/Admin</td><td>Pin/close/delete topic</td></tr><tr><td><code>POST /api/friend.php</code></td><td>Member</td><td>Friend actions (<code>action</code>, <code>other_id</code>)</td></tr><tr><td><code>POST /api/karma.php</code></td><td>Admin</td><td>Adjust karma (<code>user_id</code>, <code>amount</code>, <code>op</code>)</td></tr><tr><td><code>POST /api/notifications.php</code></td><td>Member</td><td>Mark notifications read</td></tr><tr><td><code>GET /api/search.php?q=</code></td><td>Public</td><td>Live search → <code>{topics, posts}</code></td></tr><tr><td><code>GET /api/search_users.php?q=</code></td><td>Public</td><td>User autocomplete → <code>[{id, username, avatar}]</code></td></tr><tr><td><code>POST /api/chat.php</code></td><td>Member</td><td>DM actions (<code>action</code>: send/poll/load/conversations)</td></tr></tbody></table> <p><strong>Quick example — posting a reply:</strong></p> <pre><code class="language-javascript">const fd = new FormData(); fd.append('slug', 'my-topic-slug'); fd.append('content', 'My reply content here.'); fd.append('csrf', NX.csrf); // NX is the global config object const res = await fetch(NX.base + '/api/reply.php', { method: 'POST', body: fd }); const data = await res.json(); // Success: { ok: true, post: { id, content, post_num, created_at, username, ... } } // Error: { error: "message", ... }</code></pre> <hr/> <h2 id="-developer-reference">🧰 Developer Reference</h2> <h3 id="helper-functions">Helper Functions</h3> <table class="md-table"><thead><tr><th>Function</th><th>Returns</th><th>Description</th></tr></thead><tbody><tr><td><code>e($val)</code></td><td><code>string</code></td><td><code>htmlspecialchars()</code> — always use when outputting user data</td></tr><tr><td><code>u($path)</code></td><td><code>string</code></td><td>URL with BASE prefix</td></tr><tr><td><code>asset($path)</code></td><td><code>string</code></td><td>Public asset URL</td></tr><tr><td><code>go($path)</code></td><td>never</td><td>Redirect</td></tr><tr><td><code>post($key, $default)</code></td><td><code>mixed</code></td><td><code>$_POST[$key] ?? $default</code></td></tr><tr><td><code>get($key, $default)</code></td><td><code>mixed</code></td><td><code>$_GET[$key] ?? $default</code></td></tr><tr><td><code>cfg($key, $default)</code></td><td><code>string</code></td><td>Read a setting (cached)</td></tr><tr><td><code>cfg_set($key, $value)</code></td><td>void</td><td>Write a setting</td></tr><tr><td><code>sanitise($input)</code></td><td><code>string</code></td><td>Strip HTML/PHP/scripts from user input</td></tr><tr><td><code>must_login()</code></td><td>void</td><td>Redirect if not authenticated</td></tr><tr><td><code>must_admin()</code></td><td>void</td><td>Redirect if not admin</td></tr><tr><td><code>is_admin()</code></td><td><code>bool</code></td><td>Check admin role</td></tr><tr><td><code>current_user()</code></td><td><code>?array</code></td><td>Current user row or null</td></tr><tr><td><code>csrf_input()</code></td><td><code>string</code></td><td>`<input type="hidden" name="csrf" value="...">`</td></tr><tr><td><code>csrf_ok()</code></td><td><code>bool</code></td><td>Validate CSRF token</td></tr><tr><td><code>render_post($raw)</code></td><td><code>string</code></td><td>Render Markdown + embeds to HTML</td></tr><tr><td><code>add_karma($uid, $pts)</code></td><td>void</td><td>Add (or subtract) karma points</td></tr><tr><td><code>karma_tier($karma)</code></td><td><code>array</code></td><td>Tier name, icon, colour, progress</td></tr><tr><td><code>add_notification($uid, $type, $data)</code></td><td>void</td><td>Queue a notification</td></tr><tr><td><code>can<em>read</em>category($cat)</code></td><td><code>bool</code></td><td>Read permission check</td></tr><tr><td><code>can<em>post</em>topic($cat)</code></td><td><code>bool</code></td><td>Post permission check</td></tr><tr><td><code>can<em>reply</em>topic($cat)</code></td><td><code>bool</code></td><td>Reply permission check</td></tr><tr><td><code>unique_slug($title, $table)</code></td><td><code>string</code></td><td>Generate a unique URL slug</td></tr><tr><td><code>rate_check($uid, $type)</code></td><td><code>array</code></td><td><code>{ok, wait}</code></td></tr><tr><td><code>rate_record($uid, $type)</code></td><td>void</td><td>Record a rate-limited action</td></tr><tr><td><code>addon_hook($hook, $data)</code></td><td><code>mixed</code></td><td>Fire an addon hook</td></tr></tbody></table> <h3 id="database-class">Database Class</h3> <pre><code class="language-php">DB::rows($sql, $params) // array of rows DB::row($sql, $params) // one row or null DB::insert($sql, $params) // int lastInsertId DB::run($sql, $params) // PDOStatement DB::val($sql, $params) // scalar or null DB::now() // cross-driver: NOW() or datetime('now') DB::isMysql() // bool DB::insertIgnore($table, $cols, $vals) // cross-driver INSERT IGNORE DB::upsert($table, $keyCol, $valCol, $key, $val) // cross-driver upsert</code></pre> <h3 id="adding-a-new-page">Adding a New Page</h3> <pre><code class="language-php"><?php require_once __DIR__ . '/../includes/bootstrap.php'; must_login(); // or must_admin(), or omit for public pages $PAGE_TITLE = 'My Page'; include __DIR__ . '/../views/partials/layout.php'; ?> <h1>Hello, <?= e($USER['username']) ?></h1> <p>Your karma: <?= (int)$USER['karma'] ?></p> <?php include __DIR__ . '/../views/partials/layout_end.php'; ?></code></pre> <hr/> <h2 id="-security-checklist">🛡️ Security Checklist</h2> <p>After going live:</p> <ul><li>[ ] **Delete <code>install/</code>** — prevents re-installation</li><li>[ ] **Verify <code>data/.htaccess</code>** — should deny all HTTP access (auto-created)</li><li>[ ] **Verify <code>public/uploads/.htaccess</code>** — should block <code>.php</code> execution (auto-created)</li><li>[ ] <strong>Use HTTPS</strong> — HSTS header is sent automatically when detected</li><li>[ ] <strong>MySQL users</strong> — grant only <code>SELECT</code>, <code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code> (not <code>DROP</code>)</li><li>[ ] <strong>Enable captcha</strong> — Admin → Settings → Post Captcha / Topic Captcha</li><li>[ ] <strong>Set rate limits</strong> — Admin → Settings → Rate Limiting</li></ul> <hr/> <h2 id="-faq">❓ FAQ</h2> <details> <summary><strong>Can I run this on shared hosting without shell access?</strong></summary> <p>Yes. Shared hosting is the primary target. Everything is configured through the web installer. No Composer, npm, or shell access required. </details></p> <details> <summary><strong>Do I need a separate database server?</strong></summary> <p>No. SQLite works out of the box with zero configuration — the database is a single file in <code>data/</code>. You can switch to MySQL/MariaDB any time by re-running the installer. </details></p> <details> <summary><strong>How do I upgrade to a new version?</strong></summary> <p>Replace all files except <code>data/</code>. The schema migration runs automatically on the first page load after the update, adding any new columns safely with <code>IF NOT EXISTS</code> / <code>information_schema</code> checks. </details></p> <details> <summary><strong>How do I reset a forgotten admin password?</strong></summary> <p>Run from the command line in your forum directory:</p> <pre><code class="language-bash">php -r " require 'includes/bootstrap.php'; \$hash = password_hash('new_password_here', PASSWORD_BCRYPT, ['cost' => 12]); DB::run('UPDATE users SET password=? WHERE role=?', [\$hash, 'admin']); echo 'Password reset successfully.'; "</code></pre> </details> <details> <summary><strong>Can I use this behind a reverse proxy / load balancer?</strong></summary> <p>Yes. The <code>BASE</code> path is auto-detected from <code>DOCUMENT_ROOT</code> vs <code>SCRIPT_FILENAME</code>. No <code>.env</code> changes needed. For HTTPS detection behind a proxy, ensure the proxy sets <code>X-Forwarded-Proto: https</code>. </details></p> <details> <summary><strong>How do I back up the forum?</strong></summary> <p><strong>SQLite:</strong> Copy <code>data/forum.db</code> and <code>data/db_config.php</code>.</p> <p><strong>MySQL:</strong> <code>mysqldump nexus_forum > backup.sql</code></p> <p>Also back up <code>public/uploads/</code> for user images. </details></p> <details> <summary><strong>Why no Composer / npm?</strong></summary> <p>The goal is maximum deployability. Any server running PHP 8 with PDO can run Nexus — no package manager, no build step, no Node.js. The only optional CDN dependency is Prism.js for syntax highlighting, which is lazy-loaded only when a code block is on the page. </details></p> <hr/> <h2 id="-contributing">🤝 Contributing</h2> <ol><li>Fork the repository</li><li>Create a branch: <code>git checkout -b feature/my-feature</code></li><li>Make your changes — test on <strong>both SQLite and MySQL</strong></li><li>Syntax check: <code>find . -name "*.php" | xargs php -l</code></li><li>Submit a pull request</li></ol> <h3 id="code-guidelines">Code Guidelines</h3> <ul><li><strong>PHP 8.0+</strong> — use <code>match</code>, arrow functions, named arguments freely</li><li><strong>No raw SQL interpolation</strong> — always use PDO prepared statements</li><li>**Always <code>e()</code> user output** — never echo user data unescaped</li><li><strong>Cross-driver SQL</strong> — test on both SQLite and MySQL; use <code>DB::insertIgnore()</code>, <code>DB::upsert()</code>, <code>DB::now()</code> for portability</li><li><strong>No external dependencies</strong> — no Composer packages, no npm, no build step</li></ul> <hr/> <h2 id="-license">📄 License</h2> <p>MIT License — free to use, modify, and distribute.</p> <hr/> <div align="center"> <p><strong>Nexus Discussion</strong></p> <p><em>Built with PHP 8 · PDO · Vanilla JS</em></p> <p><em>No frameworks · No build steps · No Docker required</em></p> <p>**<a href="#nexus-discussion">⬆ Back to top</a>**</p> </div> </article> </main> <footer class="foot"> <span>static pages · xgit</span> </footer> </div> </body> </html>