xgit simple git

nexus

nexus

clone git clone https://kb.hax.al/nexus

README.md

1 <div align="center">
2 
3 <img src="https://img.shields.io/badge/PHP-8.0+-777BB4?style=for-the-badge&logo=php&logoColor=white" alt="PHP 8.0+">
4 <img src="https://img.shields.io/badge/SQLite-003B57?style=for-the-badge&logo=sqlite&logoColor=white" alt="SQLite">
5 <img src="https://img.shields.io/badge/MySQL-4479A1?style=for-the-badge&logo=mysql&logoColor=white" alt="MySQL">
6 <img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="MIT License">
7 <img src="https://img.shields.io/badge/Zero-Dependencies-orange?style=for-the-badge" alt="Zero Dependencies">
8 
9 <br><br>
10 
11 ```
12 ███╗   ██╗███████╗██╗  ██╗██╗   ██╗███████╗
13 ████╗  ██║██╔════╝╚██╗██╔╝██║   ██║██╔════╝
14 ██╔██╗ ██║█████╗   ╚███╔╝ ██║   ██║███████╗
15 ██║╚██╗██║██╔══╝   ██╔██╗ ██║   ██║╚════██║
16 ██║ ╚████║███████╗██╔╝ ██╗╚██████╔╝███████║
17 ╚═╝  ╚═══╝╚══════╝╚═╝  ╚═╝ ╚═════╝ ╚══════╝
18 ```
19 
20 ### A modern, full-featured discussion platform built with pure PHP 8
21 
22 **No frameworks · No npm · No Docker · Just upload and run**
23 
24 [Features](#-features) · [Quick Start](#-quick-start) · [Installation](#-installation) · [Configuration](#️-configuration) · [Addons](#-addon-system) · [API](#-api-reference) · [FAQ](#-faq)
25 
26 </div>
27 
28 ---
29 
30 ## ✨ Features
31 
32 ### 💬 Forum Core
33 - **Categories** with icons, colours, sub-categories, and per-role permissions
34 - **Topics & threaded replies** with pagination
35 - **Full Markdown editor** — Google Docs-style toolbar with SVG icons, Write/Preview tabs
36 - **Syntax-highlighted code blocks** via Prism.js (200+ languages, lazy-loaded only when needed)
37 - **Styled blockquotes** with gradient left border
38 - **Post permalinks** — every post gets `#post-{id}` + a 🔗 copy-link button (pagination-aware)
39 - **Inline image upload** — paste, drag-drop, or file picker directly in the editor
40 - **Media auto-embeds** — paste a URL and it becomes a player (14 platforms)
41 - **@mentions** with live autocomplete
42 - **Live search** — finds topics AND post content, links directly to the matching post on the correct page
43 
44 ### 🔐 Roles & Permissions
45 
46 | Role | Level | Can Do |
47 |---|---|---|
48 | **Guest** | 0 | Read public categories |
49 | **Member** | 10 | Post, reply, like, message, friend |
50 | **Moderator** | 20 | + Pin/close topics, edit any post |
51 | **Admin** | 30 | Full access + admin panel |
52 
53 **Per-category permissions** — set independently for reading, posting, and replying:
54 
55 | Permission | Options |
56 |---|---|
57 | Who can **read** | 🌐 Everyone · 👤 Members · 🛡️ Moderators+ · 👑 Admins |
58 | Who can **post topics** | Same four options |
59 | Who can **reply** | Same four options |
60 
61 ### ⭐ Karma System
62 
63 Eight progressive tiers earned through activity:
64 
65 | Tier | Points | Icon |
66 |---|---|---|
67 | Newcomer | 0–9 | 🌱 |
68 | Member | 10–49 | 💬 |
69 | Regular | 50–99 | ⭐ |
70 | Contributor | 100–249 | 🌟 |
71 | Veteran | 250–499 | 🔥 |
72 | Expert | 500–999 | 💎 |
73 | Elite | 1000–2499 | 👑 |
74 | Legend | 2500+ | 🏆 |
75 
76 Admins can manually adjust karma (Add / Subtract / Set) with an optional reason that notifies the user.
77 
78 ### 📬 Private Messages
79 - Inbox/Sent with unread badges
80 - Conversation threads displayed as chat bubbles
81 - Read receipts (✓ sent · ✓✓ read)
82 - Online status indicator (green if active in last 5 min)
83 - Live user search autocomplete
84 
85 ### 🔍 Search
86 - **Topics tab** — title matches
87 - **Posts tab** — content matches, jumps directly to the exact post on the correct page
88 - **Users tab** — username + bio search
89 - Live header dropdown shows topic + post results simultaneously
90 
91 ### 🧩 Addon System
92 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.
93 
94 ### 🔒 Security
95 - CSRF tokens on all forms and AJAX
96 - bcrypt password hashing (cost 12)
97 - Math captcha (admin toggle, separate for posts and new topics)
98 - Rate limiting with live countdown
99 - Auto-generated `.htaccess` protection for `data/` and `uploads/`
100 - Security headers: `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, HSTS on HTTPS
101 - All SQL via PDO prepared statements
102 
103 ### 🗄️ Database Support
104 
105 | Driver | Version |
106 |---|---|
107 | **SQLite** | 3.x — zero configuration, single file |
108 | **MySQL** | 5.7+ |
109 | **MariaDB** | 10.3+ |
110 
111 Schema migrates automatically on every request — update files and existing installs upgrade themselves.
112 
113 ---
114 
115 ## 🚀 Quick Start
116 
117 ### Shared Hosting (5 minutes)
118 
119 ```bash
120 # 1. Upload to your server
121 scp -r forum-clean/ user@host:~/public_html/forum/
122 
123 # 2. Set directory permissions
124 chmod 755 data/ public/uploads/ public/uploads/avatars/
125 
126 # 3. Visit the installer
127 # https://yoursite.com/forum/install/
128 
129 # 4. Complete the 3-step wizard, then remove /install/
130 rm -rf install/
131 ```
132 
133 ### Local Development
134 
135 ```bash
136 # PHP built-in server — SQLite, zero config
137 cd forum-clean/
138 php -S localhost:8080
139 # open http://localhost:8080/install/
140 ```
141 
142 ### Docker (Apache)
143 
144 ```dockerfile
145 # Dockerfile
146 FROM php:8.2-apache
147 RUN docker-php-ext-install pdo pdo_sqlite
148 RUN a2enmod rewrite
149 COPY forum-clean/ /var/www/html/
150 RUN chown -R www-data:www-data /var/www/html/data \
151     /var/www/html/public/uploads
152 ```
153 
154 ```bash
155 docker build -t nexus-forum .
156 docker run -p 8080:80 nexus-forum
157 # open http://localhost:8080/install/
158 ```
159 
160 ---
161 
162 ## 📦 Installation
163 
164 ### Requirements
165 
166 | Item | Minimum | Notes |
167 |---|---|---|
168 | PHP | **8.0** | 8.2+ recommended |
169 | PDO | Required | `pdo_sqlite` or `pdo_mysql` |
170 | GD | Optional | For image thumbnails |
171 | Web server | Apache or Nginx | See configs below |
172 | Disk | 10 MB | Plus user uploads |
173 
174 ### Step-by-step
175 
176 **1 — Upload files**
177 
178 The forum works at any URL path:
179 - `https://yoursite.com/`
180 - `https://yoursite.com/forum/`
181 - `https://yoursite.com/community/board/`
182 
183 The `BASE` path is auto-detected. No `.env` changes needed.
184 
185 **2 — Set permissions**
186 
187 ```bash
188 chmod 755 data/
189 chmod 755 public/uploads/
190 chmod 755 public/uploads/avatars/
191 ```
192 
193 **3 — Run the web installer**
194 
195 Visit `/install/` — the 3-step wizard:
196 
197 | Step | What happens |
198 |---|---|
199 | **1 — Requirements** | Checks PHP version, extensions, directory permissions |
200 | **2 — Database** | Choose SQLite or MySQL, enter site name + admin credentials |
201 | **3 — Done** | Writes config, runs migration, shows security checklist |
202 
203 **4 — Post-install (automatic)**
204 
205 The installer automatically creates:
206 - `data/.htaccess` — denies all web access to the database directory
207 - `public/uploads/.htaccess` — blocks PHP execution in uploads folder
208 - `data/db_config.php` → `chmod 0640`
209 - `data/forum.db` → `chmod 0640` (SQLite only)
210 - `data/installed.lock` — prevents re-running the installer
211 
212 ### MySQL Setup
213 
214 ```sql
215 CREATE DATABASE nexus_forum
216   CHARACTER SET utf8mb4
217   COLLATE utf8mb4_unicode_ci;
218 
219 CREATE USER 'nexus'@'localhost' IDENTIFIED BY 'your_strong_password';
220 GRANT ALL PRIVILEGES ON nexus_forum.* TO 'nexus'@'localhost';
221 FLUSH PRIVILEGES;
222 ```
223 
224 Then select "MySQL / MariaDB" in the installer.
225 
226 ---
227 
228 ## ⚙️ Configuration
229 
230 ### Apache
231 
232 ```apache
233 <VirtualHost *:80>
234     ServerName forum.yoursite.com
235     DocumentRoot /var/www/nexus-forum
236 
237     <Directory /var/www/nexus-forum>
238         AllowOverride All
239         Require all granted
240     </Directory>
241 
242     # Protect database directory
243     <Directory /var/www/nexus-forum/data>
244         Require all denied
245     </Directory>
246 </VirtualHost>
247 ```
248 
249 ### Nginx
250 
251 ```nginx
252 server {
253     listen 80;
254     server_name forum.yoursite.com;
255     root /var/www/nexus-forum;
256     index index.php;
257 
258     # Block sensitive paths
259     location ~ ^/(data|includes)/ {
260         deny all;
261         return 404;
262     }
263     location ~ \.(db|sqlite|lock)$ {
264         deny all;
265         return 404;
266     }
267     # Block PHP execution in uploads
268     location ~ ^/public/uploads/.*\.php$ {
269         deny all;
270     }
271 
272     location ~ \.php$ {
273         fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
274         include fastcgi_params;
275         fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
276     }
277 
278     location / {
279         try_files $uri $uri/ =404;
280     }
281 }
282 ```
283 
284 ### Admin Settings Panel
285 
286 Visit **Admin → Settings** to configure:
287 
288 | Setting | Description |
289 |---|---|
290 | Site name & description | Header and `<title>` |
291 | Topics / Posts per page | Pagination sizes |
292 | Post captcha | Math captcha on replies (spam protection) |
293 | Topic captcha | Math captcha on new topics |
294 | Rate limiting | Seconds between posts |
295 | Max upload size | Image upload limit |
296 | Registration | Open or closed |
297 
298 ---
299 
300 ## 📁 Project Structure
301 
302 ```
303 nexus-forum/
304305 ├── 📁 addons/                   # Drop addon folders here
306 │   └── example-hello-world/    # Sample addon (see Addon docs)
307308 ├── 📁 admin/                   # Admin panel
309 │   ├── index.php               # Dashboard
310 │   ├── users.php               # User list
311 │   ├── user.php                # Edit user + karma manager
312 │   ├── categories.php          # Categories + role permissions
313 │   ├── topics.php              # Topic moderation
314 │   ├── settings.php            # Site settings
315 │   ├── themes.php              # Theme switching
316 │   └── addons.php              # Addon manager + developer docs
317318 ├── 📁 api/                     # JSON endpoints (POST)
319 │   ├── reply.php               # Post a reply
320 │   ├── edit.php                # Edit a post
321 │   ├── delete.php              # Delete a post
322 │   ├── like.php                # Like / unlike
323 │   ├── upload.php              # Image upload
324 │   ├── notifications.php       # Mark read
325 │   ├── friend.php              # Friend requests
326 │   ├── karma.php               # Admin karma adjust
327 │   ├── chat.php                # Private message actions
328 │   ├── search.php              # Live search (topics + posts)
329 │   ├── search_users.php        # User autocomplete
330 │   └── topic_action.php        # Pin / close / delete topic
331332 ├── 📁 auth/                    # login · register · logout
333 ├── 📁 data/                    # Created by installer (not web-accessible)
334 ├── 📁 forum/                   # category · topic · new-topic · search
335336 ├── 📁 includes/                # Core library (not web-accessible)
337 │   ├── bootstrap.php           # Loads everything, boots addons
338 │   ├── config.php              # Path detection, security headers
339 │   ├── db.php                  # PDO multi-driver DB class
340 │   ├── functions.php           # All helpers
341 │   ├── markdown.php            # Markdown + embed renderer
342 │   └── addons.php              # AddonManager class
343344 ├── 📁 install/                 # DELETE after setup
345 ├── 📁 messages/                # inbox · compose · view
346347 ├── 📁 public/
348 │   ├── css/main.css            # ~2400 lines — full design system
349 │   ├── js/app.js               # ~1000 lines — all client JS
350 │   └── uploads/                # User images (PHP execution blocked)
351352 ├── 📁 users/                   # profile · edit · search
353354 ├── 📁 views/partials/
355 │   ├── layout.php              # Header, sidebar, nav
356 │   ├── layout_end.php          # Footer, Prism.js loader, app.js
357 │   ├── admin_layout.php        # Admin sidebar
358 │   └── editor_toolbar.php      # Reusable Markdown toolbar (SVG icons)
359360 └── index.php                   # Homepage
361 ```
362 
363 ---
364 
365 ## 📺 Media Embeds
366 
367 Paste any of these URLs alone on a line in a post and it auto-embeds as a player:
368 
369 | Platform | Supported |
370 |---|---|
371 | YouTube | Videos, Shorts, YouTube Music |
372 | Vimeo | Videos |
373 | Twitch | Live streams, VODs |
374 | Dailymotion | Videos |
375 | Streamable | Clips |
376 | Rumble | Videos |
377 | Spotify | Tracks, albums, playlists, podcast episodes, artist pages |
378 | SoundCloud | Tracks |
379 | Loom | Screen recordings |
380 | CodePen | Pens |
381 | JSFiddle | Fiddles |
382 | Twitter / X | Tweets |
383 | TED Talks | Talks |
384 | Bandcamp | Tracks |
385 
386 ---
387 
388 ## 🧩 Addon System
389 
390 ### Installing
391 
392 1. Drop the addon folder into `addons/`
393 2. **Admin → Addons → ▶ Activate**
394 
395 ### Creating an Addon
396 
397 **`nexus-addon.json`** — manifest (required)
398 
399 ```json
400 {
401   "name":        "My Addon",
402   "description": "What this addon does.",
403   "version":     "1.0.0",
404   "author":      "Your Name",
405   "url":         "https://yoursite.com",
406   "hooks":       ["after_topic_created", "render_post_footer"],
407   "requires":    { "nexus": ">=14" }
408 }
409 ```
410 
411 **`main.php`** — entry point (required)
412 
413 ```php
414 <?php
415 // Runs on every request when the addon is active
416 
417 // Inject HTML below every post
418 addon_on('render_post_footer', function(array $post): string {
419     return '<div class="my-badge">✓ Verified</div>';
420 });
421 
422 // React to new topics
423 addon_on('after_topic_created', function(array $data): void {
424     // $data: topic_id, title, slug, category_id, user_id
425     // Call external webhook, send Slack message, etc.
426     // file_get_contents('https://hooks.example.com?title=' . urlencode($data['title']));
427 });
428 
429 // Filter post HTML before display
430 addon_on('render_post_content', function(string $html): string {
431     return str_replace(':-)', '😊', $html);
432 });
433 ```
434 
435 **`install.php`** — runs on activation (optional)
436 
437 ```php
438 <?php
439 DB::connect()->exec("CREATE TABLE IF NOT EXISTS my_log (
440     id         INTEGER PRIMARY KEY AUTOINCREMENT,
441     message    TEXT NOT NULL,
442     created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
443 )");
444 cfg_set('my_addon_active', '1');
445 ```
446 
447 **`uninstall.php`** — runs on deactivation (optional)
448 
449 ```php
450 <?php
451 cfg_set('my_addon_active', '0');
452 // DB::run("DROP TABLE IF EXISTS my_log");  // uncomment to clean up
453 ```
454 
455 ### Hook Reference
456 
457 | Hook | Data passed | Return | Fires when |
458 |---|---|---|---|
459 | `after_topic_created` | `array` {topic_id, title, slug, category_id, user_id} | void | Topic saved |
460 | `after_reply_saved` | `array` {post_id, topic_id, user_id} | void | Reply posted |
461 | `after_user_registered` | `array` {user_id, username, email} | void | Registration |
462 | `render_post_content` | `string` HTML | `string` HTML | Before post output |
463 | `render_post_footer` | `array` post row | `string` HTML | Below post body |
464 | `render_topic_header` | `array` topic row | `string` HTML | Above topic |
465 | `user_karma_changed` | `array` {user_id, old, new, by} | void | Karma adjusted |
466 | `admin_nav_items` | `array` items | `array` | Admin sidebar |
467 | `before_page_head` | `string` HTML | `string` HTML | Inside `<head>` |
468 
469 ### Addon PHP API
470 
471 ```php
472 // Database
473 DB::rows("SELECT * FROM topics WHERE category_id=?", [$catId]);
474 DB::row("SELECT * FROM users WHERE id=?", [$uid]);
475 DB::insert("INSERT INTO my_log (message) VALUES (?)", [$msg]);
476 DB::run("UPDATE my_table SET col=? WHERE id=?", [$val, $id]);
477 DB::val("SELECT COUNT(*) FROM posts WHERE topic_id=?", [$tid]);
478 
479 // Current user
480 global $USER; // array or null
481 
482 // Settings
483 $val = cfg('site_name', 'My Forum');
484 cfg_set('my_key', 'my_value');
485 
486 // Notifications
487 add_notification($userId, 'my_type', ['key' => 'value']);
488 
489 // Karma
490 add_karma($userId, 10);   // add 10 points
491 
492 // URL helpers
493 $url = u('forum/topic.php?slug=' . urlencode($slug));
494 $assetUrl = asset('js/app.js');
495 ```
496 
497 ---
498 
499 ## 🎨 Theming
500 
501 All design tokens are CSS custom properties in `public/css/main.css`:
502 
503 ```css
504 :root {
505   /* Brand colours */
506   --blue:    #3b82f6;
507   --blue-d:  #2563eb;
508   --blue-l:  #eff6ff;
509   --green:   #22c55e;
510   --red:     #ef4444;
511   --purple:  #8b5cf6;
512 
513   /* Surfaces */
514   --bg:       #f1f5f9;   /* page background */
515   --surface:  #ffffff;   /* cards */
516   --border:   #e2e8f0;   /* borders */
517   --border-l: #f1f5f9;   /* light borders */
518 
519   /* Text */
520   --text:   #0f172a;
521   --muted:  #64748b;
522   --faint:  #94a3b8;
523 
524   /* Typography */
525   --font: 'Inter', -apple-system, sans-serif;
526   --mono: 'JetBrains Mono', 'Fira Code', monospace;
527 
528   /* Sizing */
529   --r:    6px;    /* border radius */
530   --r-lg: 10px;
531   --r-xl: 16px;
532   --header:  56px;
533   --sidebar: 220px;
534 }
535 ```
536 
537 Override any variable in a custom stylesheet, or inject one via the `before_page_head` addon hook.
538 
539 ---
540 
541 ## 🔑 API Reference
542 
543 All endpoints accept `POST` (or `GET` for search) and expect a `csrf` parameter from the `NX.csrf` global.
544 
545 | Endpoint | Auth | Description |
546 |---|---|---|
547 | `POST /api/reply.php` | Member | Post a reply (`slug`, `content`) |
548 | `POST /api/edit.php` | Author/Admin | Edit post (`post_id`, `content`) |
549 | `POST /api/delete.php` | Author/Admin | Delete post (`post_id`) |
550 | `POST /api/like.php` | Member | Like/unlike (`post_id`) |
551 | `POST /api/upload.php` | Member | Upload image (`file`) → `{url}` |
552 | `POST /api/topic_action.php` | Mod/Admin | Pin/close/delete topic |
553 | `POST /api/friend.php` | Member | Friend actions (`action`, `other_id`) |
554 | `POST /api/karma.php` | Admin | Adjust karma (`user_id`, `amount`, `op`) |
555 | `POST /api/notifications.php` | Member | Mark notifications read |
556 | `GET  /api/search.php?q=` | Public | Live search → `{topics, posts}` |
557 | `GET  /api/search_users.php?q=` | Public | User autocomplete → `[{id, username, avatar}]` |
558 | `POST /api/chat.php` | Member | DM actions (`action`: send/poll/load/conversations) |
559 
560 **Quick example — posting a reply:**
561 
562 ```javascript
563 const fd = new FormData();
564 fd.append('slug',    'my-topic-slug');
565 fd.append('content', 'My reply content here.');
566 fd.append('csrf',    NX.csrf);  // NX is the global config object
567 
568 const res  = await fetch(NX.base + '/api/reply.php', { method: 'POST', body: fd });
569 const data = await res.json();
570 // Success: { ok: true, post: { id, content, post_num, created_at, username, ... } }
571 // Error:   { error: "message", ... }
572 ```
573 
574 ---
575 
576 ## 🧰 Developer Reference
577 
578 ### Helper Functions
579 
580 | Function | Returns | Description |
581 |---|---|---|
582 | `e($val)` | `string` | `htmlspecialchars()` — always use when outputting user data |
583 | `u($path)` | `string` | URL with BASE prefix |
584 | `asset($path)` | `string` | Public asset URL |
585 | `go($path)` | never | Redirect |
586 | `post($key, $default)` | `mixed` | `$_POST[$key] ?? $default` |
587 | `get($key, $default)` | `mixed` | `$_GET[$key] ?? $default` |
588 | `cfg($key, $default)` | `string` | Read a setting (cached) |
589 | `cfg_set($key, $value)` | void | Write a setting |
590 | `sanitise($input)` | `string` | Strip HTML/PHP/scripts from user input |
591 | `must_login()` | void | Redirect if not authenticated |
592 | `must_admin()` | void | Redirect if not admin |
593 | `is_admin()` | `bool` | Check admin role |
594 | `current_user()` | `?array` | Current user row or null |
595 | `csrf_input()` | `string` | `<input type="hidden" name="csrf" value="...">` |
596 | `csrf_ok()` | `bool` | Validate CSRF token |
597 | `render_post($raw)` | `string` | Render Markdown + embeds to HTML |
598 | `add_karma($uid, $pts)` | void | Add (or subtract) karma points |
599 | `karma_tier($karma)` | `array` | Tier name, icon, colour, progress |
600 | `add_notification($uid, $type, $data)` | void | Queue a notification |
601 | `can_read_category($cat)` | `bool` | Read permission check |
602 | `can_post_topic($cat)` | `bool` | Post permission check |
603 | `can_reply_topic($cat)` | `bool` | Reply permission check |
604 | `unique_slug($title, $table)` | `string` | Generate a unique URL slug |
605 | `rate_check($uid, $type)` | `array` | `{ok, wait}` |
606 | `rate_record($uid, $type)` | void | Record a rate-limited action |
607 | `addon_hook($hook, $data)` | `mixed` | Fire an addon hook |
608 
609 ### Database Class
610 
611 ```php
612 DB::rows($sql, $params)   // array of rows
613 DB::row($sql, $params)    // one row or null
614 DB::insert($sql, $params) // int lastInsertId
615 DB::run($sql, $params)    // PDOStatement
616 DB::val($sql, $params)    // scalar or null
617 DB::now()                 // cross-driver: NOW() or datetime('now')
618 DB::isMysql()             // bool
619 DB::insertIgnore($table, $cols, $vals)   // cross-driver INSERT IGNORE
620 DB::upsert($table, $keyCol, $valCol, $key, $val)  // cross-driver upsert
621 ```
622 
623 ### Adding a New Page
624 
625 ```php
626 <?php
627 require_once __DIR__ . '/../includes/bootstrap.php';
628 must_login();   // or must_admin(), or omit for public pages
629 
630 $PAGE_TITLE = 'My Page';
631 include __DIR__ . '/../views/partials/layout.php';
632 ?>
633 
634 <h1>Hello, <?= e($USER['username']) ?></h1>
635 <p>Your karma: <?= (int)$USER['karma'] ?></p>
636 
637 <?php include __DIR__ . '/../views/partials/layout_end.php'; ?>
638 ```
639 
640 ---
641 
642 ## 🛡️ Security Checklist
643 
644 After going live:
645 
646 - [ ] **Delete `install/`** — prevents re-installation
647 - [ ] **Verify `data/.htaccess`** — should deny all HTTP access (auto-created)
648 - [ ] **Verify `public/uploads/.htaccess`** — should block `.php` execution (auto-created)
649 - [ ] **Use HTTPS** — HSTS header is sent automatically when detected
650 - [ ] **MySQL users** — grant only `SELECT`, `INSERT`, `UPDATE`, `DELETE` (not `DROP`)
651 - [ ] **Enable captcha** — Admin → Settings → Post Captcha / Topic Captcha
652 - [ ] **Set rate limits** — Admin → Settings → Rate Limiting
653 
654 ---
655 
656 ## ❓ FAQ
657 
658 <details>
659 <summary><strong>Can I run this on shared hosting without shell access?</strong></summary>
660 
661 Yes. Shared hosting is the primary target. Everything is configured through the web installer. No Composer, npm, or shell access required.
662 </details>
663 
664 <details>
665 <summary><strong>Do I need a separate database server?</strong></summary>
666 
667 No. SQLite works out of the box with zero configuration — the database is a single file in `data/`. You can switch to MySQL/MariaDB any time by re-running the installer.
668 </details>
669 
670 <details>
671 <summary><strong>How do I upgrade to a new version?</strong></summary>
672 
673 Replace all files except `data/`. The schema migration runs automatically on the first page load after the update, adding any new columns safely with `IF NOT EXISTS` / `information_schema` checks.
674 </details>
675 
676 <details>
677 <summary><strong>How do I reset a forgotten admin password?</strong></summary>
678 
679 Run from the command line in your forum directory:
680 
681 ```bash
682 php -r "
683 require 'includes/bootstrap.php';
684 \$hash = password_hash('new_password_here', PASSWORD_BCRYPT, ['cost' => 12]);
685 DB::run('UPDATE users SET password=? WHERE role=?', [\$hash, 'admin']);
686 echo 'Password reset successfully.';
687 "
688 ```
689 </details>
690 
691 <details>
692 <summary><strong>Can I use this behind a reverse proxy / load balancer?</strong></summary>
693 
694 Yes. The `BASE` path is auto-detected from `DOCUMENT_ROOT` vs `SCRIPT_FILENAME`. No `.env` changes needed. For HTTPS detection behind a proxy, ensure the proxy sets `X-Forwarded-Proto: https`.
695 </details>
696 
697 <details>
698 <summary><strong>How do I back up the forum?</strong></summary>
699 
700 **SQLite:** Copy `data/forum.db` and `data/db_config.php`.
701 
702 **MySQL:** `mysqldump nexus_forum > backup.sql`
703 
704 Also back up `public/uploads/` for user images.
705 </details>
706 
707 <details>
708 <summary><strong>Why no Composer / npm?</strong></summary>
709 
710 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.
711 </details>
712 
713 ---
714 
715 ## 🤝 Contributing
716 
717 1. Fork the repository
718 2. Create a branch: `git checkout -b feature/my-feature`
719 3. Make your changes — test on **both SQLite and MySQL**
720 4. Syntax check: `find . -name "*.php" | xargs php -l`
721 5. Submit a pull request
722 
723 ### Code Guidelines
724 
725 - **PHP 8.0+** — use `match`, arrow functions, named arguments freely
726 - **No raw SQL interpolation** — always use PDO prepared statements
727 - **Always `e()` user output** — never echo user data unescaped
728 - **Cross-driver SQL** — test on both SQLite and MySQL; use `DB::insertIgnore()`, `DB::upsert()`, `DB::now()` for portability
729 - **No external dependencies** — no Composer packages, no npm, no build step
730 
731 ---
732 
733 ## 📄 License
734 
735 MIT License — free to use, modify, and distribute.
736 
737 ---
738 
739 <div align="center">
740 
741 **Nexus Discussion**
742 
743 *Built with PHP 8 · PDO · Vanilla JS*
744 
745 *No frameworks · No build steps · No Docker required*
746 
747 **[⬆ Back to top](#nexus-discussion)**
748 
749 </div>