
Current Version: 5.2.1 — License: GPL3 (Community & Pro)
Flatboard 5 is a complete rewrite of the forum software with a modern MVC architecture.
Editions:
| Edition | Price | Storage | Extras |
|---|---|---|---|
| Community | Free | JSON | Core features |
| Pro | €49 one-time | JSON + SQLite | Premium plugins & themes |
json, mbstring, opensslfileinfo, zip, pdo_sqlite (Pro), gd or imagick| Layer | Technology |
|---|---|
| Backend | PHP 8.0+ |
| Storage | JSON flat-file / SQLite |
| Frontend | HTML5, CSS3, JavaScript |
| Text formatting | Markdown Extra (Parsedown) |
| Library | Version | License |
|---|---|---|
| PHPMailer | 7.0.2 | LGPL-2.1 |
| Parsedown | 1.8.0 | MIT |
| Bootstrap | 5.3.8 | MIT |
| Font Awesome | 7.2.0 | Free (CC BY 4.0 / SIL OFL 1.1 / MIT) |
| SortableJS | 1.15.7 | MIT |
Special thanks to all community members who have contributed feedback, ideas, and support — your involvement makes Flatboard better for everyone.
Flatboard 5 is a modern, lightweight forum platform built with PHP 8.0+. It combines simplicity with powerful features, making it perfect for communities of all sizes.

Flatboard 5 is a complete refactoring of the forum software with a new architecture. This means:
Flatboard 5 is a next-generation forum software that offers:
These extensions are mandatory for Flatboard 5 to function:
These extensions enhance functionality but are not strictly required:
You can check your PHP version and extensions using:
# Check PHP version
php -v
# List all loaded extensions
php -m
# Check specific extension
php -m | grep json
php -m | grep mbstring
php -m | grep openssl
Or create a temporary phpinfo.php file:
<?php
phpinfo();
phpinfo.php file after checking your configuration for security reasons.Flatboard 5 is available in two distinct editions:
Free (Forever) - Licensed under GPL3
Features:
Over 10 years of continuous development. Free and regularly updated for the community.
€49 (One-time payment, no subscription) - Licensed under GPL3
Includes everything in Community Edition, plus:
Both editions are fully open source under GPL3. The Pro edition provides additional features and support, not proprietary code.
Pricing: €49 (One-time payment)
Where to Buy: flatboard.org
The installation process is straightforward and typically takes 10-15 minutes:
Before starting the installation, verify:
Once you've verified the requirements:
The main differences are:
Both editions are open source under GPL3.
Yes, you can run Flatboard 4 and Flatboard 5 on different directories or subdomains. They are completely independent installations.
Last updated: February 23, 2026
]]>json, mbstring, opensslfileinfo (for secure file uploads), zip (for archive analysis)pdo_sqlite extension requiredThe web installer is the easiest way to install Flatboard 5.
Download the latest Flatboard 5 release from flatboard.org and extract it to your web server directory:
# Download and extract ZIP file
unzip flatboard-5-latest.zip
# Or using wget
wget https://flatboard.org/download/flatboard-5-latest.zip
unzip flatboard-5-latest.zip
Set the correct permissions for directories and files:
# Directories (readable and executable)
chmod 755 app/ themes/ plugins/ languages/ vendor/
# Storage and uploads (readable, writable, executable)
chmod 750 stockage/ uploads/
# Files (readable)
chmod 644 *.php
chmod 644 public/*.php
# Configuration file (readable and writable by owner only)
chmod 600 stockage/json/config.json 2>/dev/null || true
# CLI executable
chmod 755 app/Cli/console.php
stockage/ directory contains sensitive configuration files. Ensure it's protected by .htaccess files (automatically created) to prevent direct web access.Open your browser and navigate to:
https://yourdomain.com
or
https://yourdomain.com/install.php
The installer will automatically detect if Flatboard 5 is not yet installed and redirect you to the installation wizard.
The installer will guide you through:
System Check
Site Configuration
Admin Account Creation
SMTP Configuration (Optional)
Finalization
.install.lock file after successful installation. This prevents accidental re-installation. To reinstall, delete this file.For advanced users who prefer manual setup:
Extract the Flatboard 5 archive to your web root:
cd /var/www/html # or your web root
unzip flatboard-5-latest.zip
Create the configuration directory and file:
mkdir -p stockage/json
touch stockage/json/config.json
chmod 600 stockage/json/config.json
chmod 755 app/ themes/ plugins/ languages/
chmod 750 stockage/ uploads/
chmod 644 *.php
Apache Configuration:
Ensure mod_rewrite is enabled and create/update .htaccess:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ public/index.php [QSA,L]
</IfModule>
Nginx Configuration:
server {
listen 80;
server_name yourdomain.com;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Access install.php in your browser to complete the setup.
During installation, you'll choose a storage type:
pdo_sqlite extensionCheck Admin Panel Access
/adminTest Forum Functionality
Check File Permissions
stockage/ is writableuploads/ is writableAfter installation, perform these security steps:
Remove Installer
rm install.php
Protect Configuration
chmod 600 stockage/json/config.json
Review .htaccess Files
stockage/ has .htaccess protectionuploads/ has proper restrictionsSet Debug Mode to False
Solution: Upgrade to PHP 8.0 or higher.
# Check current version
php -v
# Update PHP (Ubuntu/Debian)
sudo apt update
sudo apt install php8.1 php8.1-cli php8.1-fpm
Solution: Install missing PHP extensions.
# Ubuntu/Debian
sudo apt install php8.1-json php8.1-mbstring php8.1-openssl php8.1-fileinfo
# CentOS/RHEL
sudo yum install php-json php-mbstring php-openssl php-fileinfo
Solution: Fix directory permissions.
# Set ownership (replace www-data with your web server user)
sudo chown -R www-data:www-data /var/www/html
sudo chmod -R 755 /var/www/html
sudo chmod -R 750 /var/www/html/stockage
sudo chmod -R 750 /var/www/html/uploads
Solution: Ensure the directory is writable.
chmod 750 stockage/
chmod 750 stockage/json/
touch stockage/json/.htaccess
Solution: Check web server configuration and file paths.
public/index.php existsmod_rewrite is enabledtail -f /var/log/php-errors.logAfter successful installation:
Last updated: February 23, 2026
]]>Flatboard 5 stores its main configuration in /stockage/json/config.json.
config.json manually while the forum is running. Always use the admin panel or ensure the forum is in maintenance mode.{
"site_name": "My Forum",
"site_url": "https://example.com",
"base_url": "",
"default_language": "fr",
"timezone": "Europe/Paris",
"storage_type": "json",
"debug": false,
"maintenance_mode": false,
"registration_enabled": true,
"email_verification": false,
"pagination_type": "classic",
"smtp": {
"enabled": false,
"host": "",
"port": 587,
"username": "",
"password": "",
"from_email": "",
"from_name": ""
},
"assets": {
"minify_css": true,
"minify_js": false,
"cache_enabled": true
}
}
Access: Admin Panel > Settings > General
Site Name - Your forum's display name
Site URL - Full URL including protocol
https://example.com or http://example.comSite Description - Brief description for SEO
Base URL - Subdirectory path if installed in subfolder
/forum if installed at https://example.com/forumConfigure localization settings:
Default Language - Choose from available languages
Timezone - Set your server's timezone for correct timestamps
Europe/Paris, America/New_York, Asia/TokyoStorage Type:
Enable Registration - Allow new users to register
Email Verification - Require email verification
Default User Group - Group assigned to new users
Access: Admin Panel > Settings > Email
Configure SMTP for reliable email delivery:
smtp.gmail.com)Sensitive SMTP credentials can also be supplied via environment variables, which take precedence over config.json values:
export FLATBOARD_SMTP_HOST="smtp.example.com"
export FLATBOARD_SMTP_USERNAME="[email protected]"
export FLATBOARD_SMTP_PASSWORD="secret"Gmail:
Host: smtp.gmail.com
Port: 587
Encryption: tls
Username: [email protected]
Password: (App Password, not regular password)
Outlook/Office 365:
Host: smtp.office365.com
Port: 587
Encryption: tls
SendGrid:
Host: smtp.sendgrid.net
Port: 587
Encryption: tls
Username: apikey
Password: (Your SendGrid API key)
Use the "Send Test Email" button to verify your SMTP configuration.
Configure request rate limits:
Access: Admin Panel > Settings > Maintenance
Enable maintenance mode to perform updates or maintenance:
debug is true. A warning notice is displayed inside the section to remind administrators that these tools are exposed because debug mode is active.error, warning, info, debug/stockage/logs/Most settings can be configured through the admin panel:
/adminSome settings can be configured via command line:
# Set a configuration value
php app/Cli/console.php config:set site_name "My Forum"
# Get a configuration value
php app/Cli/console.php config:get site_name
# List all configuration
php app/Cli/console.php config:list
# Reload config from disk (discards in-memory cache — useful after manual edits)
php app/Cli/console.php config:reload
Always backup your configuration before making changes:
# Backup config.json
cp stockage/json/config.json stockage/json/config.json.backup
# Restore from backup
cp stockage/json/config.json.backup stockage/json/config.json
Possible causes:
Solution:
chmod 600 stockage/json/config.json
chown www-data:www-data stockage/json/config.json
Check:
Solution: Use proper timezone identifier:
Europe/ParisParis, GMT+1, UTC+1Last updated: February 26, 2026
]]>/admin on your forumAfter logging in, you'll see the admin dashboard with:
The admin panel is organized into sections:
Access: Admin Panel > Discussions > Categories
Access: Admin Panel > Users
Access: Admin Panel > Users > Groups
Manage user groups and permissions:
Access: Admin Panel > Discussions > Reports
Handle user reports:
Access: Admin Panel > Settings > General
Access: Admin Panel > Settings > Email
Access: Admin Panel > Settings > Security
Access: Admin Panel > Settings > Performance
Access: Admin Panel > Plugins
plugins/ directoryAccess: Admin Panel > Themes
Access: Admin Panel > Tools > Backup
Access: Admin Panel > Tools > Cache
Access: Admin Panel > Tools > Logs
Access: Admin Panel > Tools > System
Ctrl+K - Quick searchCtrl+/ - Show shortcutsEsc - Close modalsCheck:
Solution:
Solution:
Last updated: February 23, 2026
]]>Access: Admin Panel > Users
The user list displays:
Filters:
Click on a username to view:
Flatboard 5 includes these default groups:
Flatboard 5 uses a hierarchical permission system:
Permissions follow this hierarchy:
Access: Admin Panel > Users > Bans
Access user profile to see:
View detailed activity:
Check:
Solution:
Check:
Last updated: February 23, 2026
]]>Categories are the top-level organizational structure of your forum. They group related discussions together and help users find content easily.
Best Practices:
Access: Admin Panel > Discussions > Categories
General
├── Announcements (subcategory)
├── Introductions (subcategory)
└── Off-Topic (subcategory)
Support
├── Installation Help
├── Configuration
└── Troubleshooting
Development
├── Feature Requests
├── Bug Reports
└── Code Sharing
Set category-specific permissions:
As a User:
Best Practices for Titles:
Pin important discussions to the top:
Lock discussions to prevent new replies:
Move discussions between categories:
Users can sort discussions by:
Filter discussions by:
Posts are replies within discussions:
Users can react to posts:
Mention users in posts:
@username syntaxModerators can:
Tags help organize and find content:
Access: Admin Panel > Discussions > Tags
Users can search for:
Search Options:
Handle user reports:
Perform actions on multiple items:
Check:
Check:
Solution:
Last updated: February 23, 2026
]]>Plugins are self-contained extensions that:
Plugins follow a structured architecture:
plugins/
└── my-plugin/
├── plugin.json # Plugin metadata
├── MyPlugin.php # Main plugin class
├── assets/ # CSS, JS, images
│ ├── css/
│ ├── js/
│ └── img/
├── views/ # Template files
│ └── admin.php
└── README.md # Documentation
.zip archive — no FTP or shell access required..zip from the Flatboard Resource Center or from the plugin author.zip archive and confirm — the archive is validated, extracted, and the plugin appears in the list automaticallyThe server validates the archive integrity, MIME type, size, and the presence of a valid plugin.json before extracting.
Plugin packages are distributed as .zip archives. The manual workflow is: download → extract locally → upload the folder → activate.
.zip from the Flatboard Resource Center or from the plugin authormy-plugin/) with plugin.json at its root.zip file itself to the server. FTP transfers files individually — you must extract the archive first, then upload the resulting folder.Connect to your server with an FTP client (FileZilla, Cyberduck, etc.) and upload the extracted plugin folder into the plugins/ directory of your Flatboard installation:
your-flatboard/
└── plugins/
└── my-plugin/ ← upload this folder (not the .zip)
├── plugin.json
└── ...
If you have shell access, you can extract the archive directly on the server:
cd /path/to/your-flatboard/plugins/
unzip my-plugin.zip # extracts to my-plugin/
chmod 755 my-plugin/ -R # adjust permissions if needed
Once the plugin folder is in place:
If the plugin has settings, click the settings icon next to the plugin name, fill in the options, and save.
Plugins from Flatboard 3 and 4 are NOT compatible with Flatboard 5 due to the complete architecture rewrite. You'll need to:
mkdir -p plugins/my-plugin/{assets/{css,js,img},views}
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"description": "A custom plugin for Flatboard 5",
"author": "Your Name",
"license": "GPL3",
"requires": {
"flatboard": ">=5.0.0"
},
"class": "App\\Plugins\\MyPlugin\\MyPluginPlugin"
}
plugins/my-plugin/MyPluginPlugin.php:
<?php
namespace App\Plugins\MyPlugin;
use App\Core\Plugin;
class MyPluginPlugin
{
public function boot()
{
// Register hooks
Plugin::hook('view.header.content', [$this, 'addHeaderContent']);
Plugin::hook('app.routes.register', [$this, 'registerRoutes']);
}
public function addHeaderContent(&$content)
{
$content .= '<link rel="stylesheet" href="proxy.php?url=/plugins/my-plugin/assets/css/style.css">';
}
public function registerRoutes($router)
{
$router->get('/my-plugin', function() {
return 'Hello from my plugin!';
});
}
}
Flatboard 5 provides many hooks:
app.routes.register - Register custom routesview.header.styles - Add CSS styles to headerview.footer.content - Add content to footerview.post.avatar - Override post avatar renderingview.user.avatar - Override user avatar rendering anywhere on the forumdiscussion.created - When discussion is createdpost.created - When post is createduser.registered - When user registersuser.updated - When user is updatednotification.types.register - Register custom notification typesvisitor.page_info - Provide page metadata for presence indicatorSee the complete hook list below for all available hooks.
Complete reference of all 74 hooks available in Flatboard 5. All data arguments marked by ref are passed by PHP reference — modifying them changes the actual value used by the framework.
| Hook | Trigger point | Data |
|---|---|---|
app.routes.register |
Application bootstrap | Router instance |
router.plugins.register |
Router dispatch | Router instance |
security.csp.directives |
CSP header generation | CSP directives array by ref |
| Hook | Trigger point | Data |
|---|---|---|
view.header.styles |
<head> (frontend + backend) |
Styles array by ref |
view.footer.scripts |
</body> (frontend + backend) |
Scripts array by ref |
view.footer.content |
Footer (frontend) | HTML content array by ref |
view.footer.links |
Footer links (frontend) | Links array by ref |
view.navbar.items |
Main navigation bar | Nav items array by ref |
view.main.before |
Before main content (frontend) | HTML string by ref |
view.main.after |
After main content (frontend) | HTML string by ref |
view.admin.sidebar.items |
Admin sidebar | Sidebar items array by ref |
view.admin.main.before |
Before admin main content | HTML string by ref |
view.admin.main.after |
After admin main content | HTML string by ref |
| Hook | Trigger point | Data |
|---|---|---|
view.banner.data |
Banner component render | Banner data array by ref |
view.banner.content |
Banner component render | HTML string by ref, banner data |
| Hook | Trigger point | Data |
|---|---|---|
seo.meta.before |
<head> meta tags |
SEO data array by ref |
seo.meta.after |
After default meta tags | Additional meta HTML string by ref |
| Hook | Trigger point | Data |
|---|---|---|
view.login.form |
Login page + login modal | Form fields array by ref |
view.login.validation |
Login form submission | Validation error by ref |
view.user.register.form |
Registration page + modal | Form fields array by ref |
view.user.register.validation |
Registration form submission | Validation error by ref |
| Hook | Trigger point | Data |
|---|---|---|
view.discussion.create.form |
New discussion form | Form fields array by ref |
view.discussion.create.validation |
Discussion creation submission | Validation error by ref |
view.discussion.show.before_content |
Discussion view (before first post) | Event data array by ref |
view.discussion.show.after_content |
Discussion view (after first post) | Event data array by ref |
view.reply.create.form |
Reply form | Form fields array by ref |
view.reply.create.validation |
Reply submission | Validation error by ref |
view.sidebar.navigation |
Discussion list sidebar | Sidebar items array by ref |
view.post.avatar |
Post thread component — avatar render | ['user_id', 'username', 'avatar', 'size', 'html'] by ref — set html to override the default <img> output |
view.user.avatar |
components/avatar.php and discussion-item list (author + last-post author) |
['user_id', 'username', 'avatar', 'size', 'html'] by ref — set html to override the rendered avatar for any user across the forum |
| Hook | Trigger point | Data |
|---|---|---|
discussion.created |
After discussion saved | Enriched event data array |
discussion.created.legacy |
After discussion saved | Created discussion (legacy format) |
discussion.deleted |
After discussion deleted | Discussion data |
best_answer.set |
Best answer selected | ['discussion_id', 'post_id', 'user_id'] |
post.created |
After reply saved | Created post data |
post.updated |
After reply edited | Updated post data |
post.deleted |
After reply deleted | Post data |
category.created |
After category saved | Created category data |
category.updated |
After category updated | Updated category data |
category.deleted |
After category deleted | Category data |
tag.created |
After tag saved | Tag data |
| Hook | Trigger point | Data |
|---|---|---|
user.registered |
After registration | Created user data |
user.updated |
After user profile saved | Updated user data |
user.deleted |
After user account deleted | User data |
ban.created |
After ban applied | Updated user data |
ban.removed |
After ban lifted | Updated user data |
user.profile.tabs |
Profile page render | Tabs array by ref, user array, isOwnProfile bool |
user.profile.tab.contents |
Profile page render | Tab contents array by ref, user array, isOwnProfile bool |
user.settings.notifications |
Notifications settings form | Options array by ref, preferences array |
user.settings.notifications.save |
Notifications settings save | Preferences array by ref, request data, user |
user.settings.preferences.save |
General preferences save | Preferences array by ref, request data, user |
| Hook | Trigger point | Data |
|---|---|---|
report.created |
After content report saved | Created report data |
| Hook | Trigger point | Data |
|---|---|---|
search.results |
Search service (all types) | Results array by ref, query, type, keywords, filters |
search.before.display |
Before HTML search results | Results array by ref, query, type, filters |
search.ajax.results |
Before AJAX search results | Results array by ref, query, type, filters |
| Hook | Trigger point | Data |
|---|---|---|
notification.types.register |
NotificationService::validateNotificationType() — called at boot time |
$allowedTypes array by ref — append custom type strings to extend the core whitelist |
notification.before.create |
Before notification written | Notification data array by ref, type string |
notification.after.create |
After notification written | Notification ID, notification data |
notification.email.body |
Before notification email sent | Email body string by ref, notification data |
| Hook | Trigger point | Data |
|---|---|---|
markdown.parse |
Markdown rendering (all contexts) | Result array by ref, raw markdown string |
markdown.editor.config |
Editor component init | Config array by ref, $editorId |
component.markdown-editor.before |
Before editor textarea | HTML string by ref, $editorId |
component.markdown-editor.attributes |
Editor textarea tag | Attributes array by ref, $editorId, config |
markdown.editor.init |
Editor JS init script | Script string by ref, $editorId, config |
markdown.editor.getValue |
Editor getValue script | Script string by ref, $editorId |
markdown.editor.setValue |
Editor setValue script | Script string by ref, $editorId, value path |
component.markdown-editor.after |
After editor textarea | HTML string by ref, $editorId |
| Hook | Trigger point | Data |
|---|---|---|
visitor.page_info |
Visitor::getPageInfo() — unknown URLs only |
['page' => string, 'info' => array] by ref — set info['type'], info['title'], info['url'] |
visitor.before_track |
VisitorTrackingMiddleware before write |
['ip_address', 'user_agent', 'page', 'user_id'] by ref |
presence.anonymous_visitors |
PresenceService::getActiveAnonymousVisitorsDetailed() |
Visitors array by ref |
presence.bots |
PresenceService::getActiveBotsDetailed() |
Bots array by ref |
presence.users |
PresenceService::getActiveUsersDetailed() |
Users array by ref |
| Hook | Trigger point | Data |
|---|---|---|
admin.dashboard.widgets |
Admin dashboard render | Widgets array by ref |
| Hook | Trigger point | Data |
|---|---|---|
plugin.view.index |
Plugin index view rendered | ['pluginId', 'viewName', 'viewVars'] by ref |
plugin.view.{viewName} |
Named plugin view rendered | ['pluginId', 'viewName', 'viewVars'] by ref |
plugin.view.index.permissions |
Before plugin index view | Permissions array by ref, pluginId, 'index' |
plugin.view.{viewName}.permissions |
Before named plugin view | Permissions array by ref, pluginId, viewName |
plugin.view.{viewName}.vars |
Before named plugin view | ViewVars array by ref, pluginId, viewName |
plugin.view.admin.vars |
Before plugin admin view | ViewVars array by ref, pluginId, 'admin' |
plugin.view.privacy.vars |
Before plugin privacy view | ViewVars array by ref, pluginId, 'privacy' |
plugin.view.cookies.vars |
Before plugin cookies view | ViewVars array by ref, pluginId, 'cookies' |
| Hook | Trigger point | Data |
|---|---|---|
webhook.received |
Inbound webhook payload | Webhook data array |
// Register a hook
Plugin::hook('discussion.created', function($discussion) {
// Do something when discussion is created
Logger::info("New discussion: " . $discussion['title']);
});
// Register with priority
Plugin::hook('post.created', [$this, 'handlePostCreated'], 10);
// Extend the notification type whitelist (by reference)
Plugin::hook('notification.types.register', function(array &$types): void {
$types[] = 'my_custom_type';
$types[] = 'reputation_badge';
});
// Override avatar rendering for a virtual user (by reference)
Plugin::hook('view.user.avatar', function(array &$data): void {
if ($data['user_id'] === 'my-bot') {
$data['html'] = '<span class="avatar-bot"><i class="fas fa-robot"></i></span>';
}
});
Plugin settings are stored in the "plugin" section of plugin.json and accessed via Plugin::getData(). Do not use Config::get/set for plugin-specific settings.
use App\Core\Plugin;
// Read a setting (with optional default)
$value = Plugin::getData('my-plugin', 'setting', 'default');
// Dot-notation is supported for nested keys
$host = Plugin::getData('my-plugin', 'smtp.host', '');
// Write a setting (in-memory only)
Plugin::setData('my-plugin', 'setting', 'value');
// Persist all settings to plugin.json
Plugin::saveData('my-plugin', ['setting' => 'value', 'another' => 'data']);
Create admin settings page:
views/admin.phpUninstalling permanently removes a plugin and all its data from your forum.
uninstall() hook (if defined), then deactivate() hookplugins.enabledplugins/stockage/ (JSON or SQLite) is not automatically removed — consult the plugin's documentation for manual cleanup steps if needed.
Always create a backup before uninstalling a plugin you may want to restore later.
cantDisable = "1" in plugin.json) cannot be uninstalled — the button is hiddenPlugins can run cleanup logic when uninstalled:
public function uninstall(): void
{
// Clean up plugin-specific data in stockage/, custom tables, etc.
// Called before the plugin directory is deleted
}
public function deactivate(): void
{
// Called when plugin is deactivated (also called during uninstall)
}
Some plugins require other plugins:
{
"requires": {
"flatboard": ">=5.0.0",
"plugins": {
"another-plugin": ">=1.0.0"
}
}
}
Check:
plugin.json is validSolution:
Solution:
Last updated: March 10, 2026
]]>Themes control the visual appearance and layout of your forum:
themes/
└── my-theme/
├── theme.json # Theme metadata
├── assets/ # CSS, JS, images
│ ├── css/
│ │ ├── frontend.css
│ │ └── admin.css
│ ├── js/
│ │ └── main.js
│ └── img/
│ └── logo.png
└── views/ # Template overrides (optional)
└── layouts/
Every theme requires a theme.json file:
{
"name": "My Theme",
"id": "my-theme",
"version": "1.0.0",
"description": "A custom theme for Flatboard 5",
"author": "Your Name",
"author_url": "https://example.com",
"author_email": "[email protected]",
"screenshot": "screenshot.png",
"requires": {
"flatboard": ">=5.0.0"
},
"styles": [
"assets/css/frontend.css"
],
"scripts": [
"assets/js/main.js"
],
"variables": {
"primary_color": "#007bff",
"secondary_color": "#6c757d"
},
"hide_color_settings": false,
"theme_settings": {},
"form_config": {}
}
Hide Color Settings
If your theme manages colors differently (e.g., using CSS variables or external frameworks), you can hide the color customization panel:
{
"hide_color_settings": true
}
When set to true, the color settings section will not appear in the admin panel. This is useful for themes like Bootswatch that manage colors through their own system.
Advanced Theme Settings
Add custom configuration options that appear in the admin panel:
{
"theme_settings": {
"layout_style": "default",
"enable_animations": true,
"custom_option": "value"
}
}
Form Configuration (form_config)
Create advanced configuration forms with multiple field types:
{
"form_config": {
"form_id": "my_theme_config",
"form_title": "Theme Configuration",
"submit_text": "Save Settings",
"fields": {
"layout_style": {
"type": "select",
"label": "Layout Style",
"description": "Choose the layout style",
"default": "default",
"category": "general",
"options": {
"default": "Default",
"wide": "Wide",
"compact": "Compact"
}
},
"enable_animations": {
"type": "checkbox",
"label": "Enable Animations",
"description": "Enable smooth animations",
"default": true,
"category": "general"
},
"custom_html": {
"type": "textarea",
"label": "Custom HTML",
"description": "Add custom HTML content",
"default": "",
"category": "advanced"
}
},
"categories": {
"general": "General",
"advanced": "Advanced"
}
}
}
Supported field types:
select - Dropdown menu with optionscheckbox - Boolean checkboxtextarea - Multi-line text input (supports HTML)text - Single-line text inputAssetMinifier (unless .min.* files are provided)AssetLoader with automatic cache invalidationmkdir -p themes/my-theme/assets/{css,js,img}
Create the theme configuration file with metadata.
assets/css/frontend.css:
/* Theme Variables */
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--background-color: #ffffff;
--text-color: #212529;
--border-color: #dee2e6;
}
/* Global Styles */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
}
/* Header */
.header {
background-color: var(--primary-color);
color: white;
padding: 1rem;
}
/* Navigation */
.nav {
display: flex;
gap: 1rem;
}
/* Buttons */
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
background-color: var(--primary-color);
color: white;
cursor: pointer;
}
.btn:hover {
opacity: 0.9;
}
assets/js/main.js:
// Theme JavaScript
document.addEventListener('DOMContentLoaded', function() {
// Your theme-specific JavaScript
console.log('Theme loaded!');
});
Use CSS variables for easy customization:
:root {
--theme-primary: #007bff;
--theme-secondary: #6c757d;
--theme-background: #ffffff;
--theme-text: #212529;
}
Support light/dark modes:
/* Light mode */
:root {
--bg-color: #ffffff;
--text-color: #000000;
}
/* Dark mode */
[data-theme="dark"] {
--bg-color: #1a1a1a;
--text-color: #ffffff;
}
Use media queries for mobile support:
/* Desktop */
.container {
max-width: 1200px;
margin: 0 auto;
}
/* Tablet */
@media (max-width: 768px) {
.container {
max-width: 100%;
padding: 0 1rem;
}
}
/* Mobile */
@media (max-width: 480px) {
.container {
padding: 0 0.5rem;
}
}
.zip archive — no FTP or shell access required..zip from the Flatboard Resource Center or from the theme author.zip archive and confirm — the archive is validated, extracted, and the theme appears in the list automaticallyThe server validates the archive integrity, MIME type, size, and the presence of a valid theme.json before extracting.
The manual workflow is: download → extract locally → upload the folder → activate.
.zip from the Flatboard Resource Center or from the theme authormy-theme/) with theme.json at its root.zip file to the server directly. Extract the archive first, then upload the resulting folder.Connect to your server with an FTP client (FileZilla, Cyberduck, etc.) and upload the extracted theme folder into the themes/ directory of your Flatboard installation:
your-flatboard/
└── themes/
└── my-theme/ ← upload this folder (not the .zip)
├── theme.json
└── assets/
If you have shell access, extract directly on the server:
cd /path/to/your-flatboard/themes/
unzip my-theme.zip # extracts to my-theme/
Once the folder is uploaded, go to Admin → Themes, find your theme in the list, and click Activate. The theme is applied immediately.
If needed, you can manually set the active theme by editing config.json:
{
"theme": {
"active": "my-theme"
}
}
Create custom templates:
themes/my-theme/views/
├── layouts/
│ └── main.php
├── discussions/
│ └── list.php
└── posts/
└── view.php
Flatboard 5 checks in this order:
Flatboard 5 includes several default themes:
form_config)hide_color_settings: true) - colors managed by Bootswatch systemMost themes support admin customization through the admin panel:
hide_color_settings is enabled)theme_settingsform_config for complex settings with multiple field typesBy default, themes expose color variables that can be customized in the admin panel:
"hide_color_settings": true in theme.json to hide the color customization panel.Themes can define advanced configuration forms using form_config:
These settings are automatically saved and can be accessed in your theme's PHP templates via the theme configuration system.
Check:
theme.json is validSolution:
Solution:
Themes from Flatboard 3 and 4 are NOT compatible with Flatboard 5 due to the complete architecture rewrite. You'll need to:
Once you've created your theme, you can share it with the Flatboard community by publishing it in the Flatboard Resource Center.
Before submitting your theme, ensure:
theme.json is complete and validscreenshot.png)After approval, your theme will be:
Last updated: March 12, 2026
]]>Flatboard 5 includes multiple security layers:
Cross-Site Request Forgery (CSRF) attacks trick users into performing actions they didn't intend.
Rate limiting prevents:
Access: Admin Panel > Settings > Security
Configure limits for:
Flatboard 5 sends rate limit headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640995200
All user input is sanitized:
Grant minimum necessary permissions:
Configure strong password requirements:
Flatboard 5 uses:
Enable 2FA for additional security:
// Allowed file types
$allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
// Maximum file size (2MB)
$maxSize = 2 * 1024 * 1024;
// Upload directory
$uploadDir = 'uploads/attachments/';
Flatboard 5 enforces layered session protection via App\Core\Session:
| Constant | Value | Effect |
|---|---|---|
REGENERATE_INTERVAL |
1800 s | Session ID is automatically regenerated every 30 minutes |
IDLE_TIMEOUT |
3600 s | Session expires after 1 hour of inactivity |
MAX_LIFETIME |
86400 s | Absolute session maximum regardless of activity (24 hours) |
REMEMBER_ME_LIFETIME |
2592000 s | Cookie and session lifetime when "Remember Me" is checked (30 days) |
Session fingerprinting — on each request, a fingerprint is computed as SHA-256(User-Agent + Accept-Language). If the fingerprint changes mid-session, the session is immediately invalidated to prevent hijacking. IP validation is also performed by default and can be toggled for CDN/mobile environments:
Session::disableIpValidation(); // e.g., for users behind a CDN or mobile network
Session::enableIpValidation(); // default — re-enable IP binding
Available for plugin developers:
// Get and immediately remove a value (useful for one-time tokens)
$value = Session::pull('key', $default);
// Atomically increment/decrement a session counter
Session::increment('login_attempts', 1);
Session::decrement('credits', 1);
// Set a flash value visible only on the NEXT request
Session::flashNext('success', 'Saved!');
$message = Session::getFlash('success');
// Get only user-scoped session data (excludes internal _security, _remember_me, etc.)
$userData = Session::allUser();
// Session metadata for debugging
$meta = Session::metadata();
// Returns: id, started_at, last_activity, fingerprint, age, idle_time
// Migrate session data to a new ID without destroying the old one
Session::migrate();
Set correct permissions:
# Directories
chmod 755 app/ themes/ plugins/ languages/
chmod 750 stockage/ uploads/
# Files
chmod 644 *.php
chmod 600 stockage/json/config.json
Protect sensitive directories:
# .htaccess in stockage/
<FilesMatch "\.(json|log)$">
Deny from all
</FilesMatch>
Secure PHP settings:
; Disable dangerous functions
disable_functions = exec,passthru,shell_exec,system
; Hide PHP version
expose_php = Off
; Error reporting (production)
display_errors = Off
log_errors = On
Add security headers:
# Apache .htaccess
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header set X-XSS-Protection "1; mode=block"
Header set Referrer-Policy "strict-origin-when-cross-origin"
# Nginx configuration
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
Regular security audits:
Last updated: February 23, 2026
]]>Flatboard 5 is designed for performance:
Flatboard 5 caches rendered Markdown:
Enable/Disable:
Cache compiled templates:
Clear Cache:
Rebuild Cache:
# Clear cache via CLI
php app/Cli/console.php cache:clear
# Rebuild cache
php app/Cli/console.php cache:rebuild
Flatboard 5 automatically minifies assets:
Configuration:
Optimize images before upload:
Use a CDN for static assets:
Optimize JSON storage:
Optimize SQLite database:
-- Vacuum database
VACUUM;
-- Analyze tables
ANALYZE;
-- Reindex
REINDEX;
Via Admin Panel:
Regular cleanup tasks:
# Clean old cache files
php app/Cli/console.php cleanup:cache
# Clean empty discussions
php app/Cli/console.php cleanup:empty-discussions
# Clean old storage
php app/Cli/console.php cleanup:storage
Optimize PHP settings:
; Increase memory limit
memory_limit = 256M
; Optimize opcache
opcache.enable = 1
opcache.memory_consumption = 128
opcache.max_accelerated_files = 10000
; Increase execution time if needed
max_execution_time = 60
Apache:
# Enable compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript
</IfModule>
# Enable caching
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
</IfModule>
Nginx:
# Enable compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
# Enable caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
Ensure proper indexes:
-- Check indexes
.schema
-- Create indexes if needed
CREATE INDEX idx_discussion_category ON discussions(category_id);
CREATE INDEX idx_post_discussion ON posts(discussion_id);
CREATE INDEX idx_user_email ON users(email);
Optimize queries:
Monitor key metrics:
Browser DevTools:
Server Monitoring:
Test performance:
Check:
Solution:
Solution:
Last updated: February 23, 2026
]]>Flatboard 5 uses email for:
SMTP (Simple Mail Transfer Protocol) provides:
Access: Admin Panel > Settings > Email
Host: smtp.gmail.com
Port: 587
Encryption: tls
Username: [email protected]
Password: (App Password - see below)
Gmail requires an "App Password" instead of your regular password:
Host: smtp.office365.com
Port: 587
Encryption: tls
Username: [email protected]
Password: (Your account password)
Host: smtp.sendgrid.net
Port: 587
Encryption: tls
Username: apikey
Password: (Your SendGrid API key)
Host: smtp.mailgun.org
Port: 587
Encryption: tls
Username: (Your Mailgun SMTP username)
Password: (Your Mailgun SMTP password)
Host: email-smtp.region.amazonaws.com
Port: 587
Encryption: tls
Username: (Your SES SMTP username)
Password: (Your SES SMTP password)
Use the built-in test email:
Test via CLI:
php app/Cli/console.php email:test [email protected]
If test email fails:
# Test SMTP connection
telnet smtp.gmail.com 587
Email templates can be customized:
Use variables in templates:
{site_name} - Forum name{site_url} - Forum URL{user_name} - User's name{reset_link} - Password reset link{verification_link} - Email verification linkUsers can request new verification email:
Configure reset email:
Users receive emails for:
Users can configure:
Access: User Profile > Settings > Notifications
Improve email deliverability:
Check:
Common Issues:
Solutions:
Some providers limit sending:
For high-volume sites, consider:
For better performance:
Enable email logging:
Last updated: February 23, 2026
]]>https://yourdomain.com/api
Current API version: v1
All endpoints are prefixed with /api/v1/
All responses are in JSON format:
{
"success": true,
"data": {
// Response data
},
"message": "Operation successful"
}
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Error description"
}
}
Generate API keys:
Include API key in requests:
Header:
X-API-Key: your-api-key-here
Query Parameter:
?api_key=your-api-key-here
OAuth 2.0 support for more advanced authentication.
API requests are rate limited:
X-RateLimit-Limit - Request limitX-RateLimit-Remaining - Remaining requestsX-RateLimit-Reset - Reset timestampGET /api/v1/discussions
Parameters:
category - Filter by category IDpage - Page number (default: 1)limit - Results per page (default: 20)sort - Sort order (latest, oldest, popular)Response:
{
"success": true,
"data": {
"discussions": [
{
"id": "123",
"title": "Discussion Title",
"slug": "discussion-title",
"category_id": "456",
"author": {
"id": "789",
"username": "john_doe"
},
"created_at": "2025-12-25T10:00:00Z",
"reply_count": 5,
"view_count": 100
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 50,
"pages": 3
}
}
}
GET /api/v1/discussions/{id}
Response:
{
"success": true,
"data": {
"discussion": {
"id": "123",
"title": "Discussion Title",
"content": "Discussion content...",
"category_id": "456",
"author": {
"id": "789",
"username": "john_doe",
"avatar": "https://example.com/avatar.jpg"
},
"created_at": "2025-12-25T10:00:00Z",
"updated_at": "2025-12-25T11:00:00Z",
"reply_count": 5,
"view_count": 100,
"tags": ["tag1", "tag2"]
}
}
}
POST /api/v1/discussions
Request Body:
{
"title": "New Discussion",
"content": "Discussion content...",
"category_id": "456",
"tags": ["tag1", "tag2"]
}
Response:
{
"success": true,
"data": {
"discussion": {
"id": "123",
"title": "New Discussion",
// ... full discussion object
}
}
}
GET /api/v1/discussions/{discussion_id}/posts
Parameters:
page - Page numberlimit - Results per pageGET /api/v1/posts/{id}
POST /api/v1/discussions/{discussion_id}/posts
Request Body:
{
"content": "Post content..."
}
GET /api/v1/users/{id}
GET /api/v1/users
Parameters:
page - Page numberlimit - Results per pagesearch - Search by usernameGET /api/v1/categories
GET /api/v1/categories/{id}
// Get discussions
fetch('https://yourdomain.com/api/v1/discussions', {
headers: {
'X-API-Key': 'your-api-key'
}
})
.then(response => response.json())
.then(data => {
console.log(data);
});
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://yourdomain.com/api/v1/discussions');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: your-api-key'
]);
$response = curl_exec($ch);
$data = json_decode($response, true);
curl_close($ch);
import requests
headers = {
'X-API-Key': 'your-api-key'
}
response = requests.get(
'https://yourdomain.com/api/v1/discussions',
headers=headers
)
data = response.json()
Common error codes:
400 - Bad Request401 - Unauthorized403 - Forbidden404 - Not Found429 - Too Many Requests500 - Internal Server ErrorFlatboard 5 provides a presence update endpoint used by the frontend heartbeat to track the current visitor's page in real time.
POST /presence/update (authenticated users — CSRF protected)
POST /api/presence/update (API clients)
Request Body:
{
"page": "/d/123/discussion-title"
}
Response:
{ "success": true }
Flatboard 5 includes a built-in webhook system for real-time notifications to external services. Configure webhooks at Admin Panel → Webhooks.
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/webhooks |
Receive inbound webhook payloads |
GET |
/admin/webhooks |
List configured webhooks |
GET |
/admin/webhooks/history |
View delivery history |
POST |
/admin/webhooks/save |
Create or update a webhook |
POST |
/admin/webhooks/test |
Send a test payload to a webhook |
GET |
/admin/webhooks/stats |
Webhook delivery statistics |
discussion.created — New discussion publishedpost.created — New reply publisheduser.registered — New user registeredreport.created — Content report submittedEach event sends a POST request to the configured URL with a JSON body and an X-Flatboard-Event header identifying the event type. Delivery history and retry status are available in the admin panel.
Last updated: February 23, 2026
]]>/stockage/json/pdo_sqlite extensionJSON storage uses organized file structure:
stockage/json/
├── config.json # Configuration
├── users/ # User data
│ ├── user_123.json
│ └── user_456.json
├── categories/ # Categories
│ ├── category_1.json
│ └── category_2.json
├── discussions/ # Discussions
│ ├── discussion_1.json
│ └── discussion_2.json
└── posts/ # Posts
├── post_1.json
└── post_2.json
Example user file (users/user_123.json):
{
"id": "123",
"username": "john_doe",
"email": "[email protected]",
"password_hash": "$2y$10$...",
"created_at": "2025-12-25T10:00:00Z",
"group_id": "member",
"reputation": 100
}
SQLite uses standard relational database:
-- Users table
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL,
group_id TEXT NOT NULL,
reputation INTEGER DEFAULT 0
);
-- Discussions table
CREATE TABLE discussions (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
category_id TEXT NOT NULL,
author_id TEXT NOT NULL,
created_at TEXT NOT NULL,
reply_count INTEGER DEFAULT 0,
view_count INTEGER DEFAULT 0
);
pdo_sqliteFor advanced users:
-- Vacuum database
VACUUM;
-- Analyze tables
ANALYZE;
-- Reindex
REINDEX;
-- Optimize
PRAGMA optimize;
Via Admin Panel:
# Backup JSON storage
tar -czf backup-json-$(date +%Y%m%d).tar.gz stockage/json/
# Restore JSON storage
tar -xzf backup-json-20251225.tar.gz
# Backup SQLite database
sqlite3 stockage/sqlite/flatboard.db ".backup backup.db"
# Restore SQLite database
sqlite3 stockage/sqlite/flatboard.db < backup.sql
Set up automated backups:
#!/bin/bash
# Backup script
DATE=$(date +%Y%m%d)
BACKUP_DIR="/backups"
# JSON backup
tar -czf $BACKUP_DIR/json-$DATE.tar.gz stockage/json/
# SQLite backup (if Pro)
if [ -f "stockage/sqlite/flatboard.db" ]; then
sqlite3 stockage/sqlite/flatboard.db ".backup $BACKUP_DIR/sqlite-$DATE.db"
fi
# Clean old cache
php app/Cli/console.php cleanup:cache
# Clean empty discussions
php app/Cli/console.php cleanup:empty-discussions
# Clean old storage
php app/Cli/console.php cleanup:storage
JSON:
SQLite:
pdo_sqlite extensionJSON:
SQLite:
Last updated: February 23, 2026
]]>Error: "PHP version 8.0 or higher required"
Solution:
# Check current version
php -v
# Update PHP (Ubuntu/Debian)
sudo apt update
sudo apt install php8.1 php8.1-cli php8.1-fpm
# Update PHP (CentOS/RHEL)
sudo yum install php81 php81-php-cli php81-php-fpm
Error: "Required PHP extension missing"
Solution:
# Install required extensions (Ubuntu/Debian)
sudo apt install php8.1-json php8.1-mbstring php8.1-openssl php8.1-fileinfo
# Install required extensions (CentOS/RHEL)
sudo yum install php-json php-mbstring php-openssl php-fileinfo
Error: "Permission denied" or "Cannot write to directory"
Solution:
# Set ownership (replace www-data with your web server user)
sudo chown -R www-data:www-data /var/www/html
sudo chmod -R 755 /var/www/html
sudo chmod -R 750 /var/www/html/stockage
sudo chmod -R 750 /var/www/html/uploads
Symptoms: Blank page or 404 error
Solution:
public/index.php existsmod_rewrite is enabledtail -f /var/log/php-errors.logSymptoms: Changes not persisting
Solution:
# Check file permissions
chmod 600 stockage/json/config.json
chown www-data:www-data stockage/json/config.json
# Check disk space
df -h
# Check error logs
tail -f stockage/logs/error.log
Symptoms: Emails not delivered
Check:
Solution:
telnet smtp.gmail.com 587Symptoms: Incorrect timestamps
Solution:
Europe/Paris (not Paris)php -i | grep timezoneSymptoms: Pages load slowly
Check:
Solution:
# Enable caching
# Admin Panel > Settings > Performance > Enable Cache
# Clear cache
php app/Cli/console.php cache:clear
# Optimize assets
# Admin Panel > Settings > Performance > Asset Optimization
Symptoms: PHP memory errors
Solution:
# Increase PHP memory limit (php.ini)
memory_limit = 256M
Symptoms: Slow queries
Solution:
-- Vacuum database
VACUUM;
-- Analyze tables
ANALYZE;
-- Reindex
REINDEX;
Symptoms: Login fails
Check:
Solution:
Symptoms: "You don't have permission"
Solution:
Symptoms: "Permission denied" when posting
Check:
Solution:
Symptoms: Plugin doesn't activate
Check:
plugin.json is validSolution:
# Check plugin structure
ls -la plugins/my-plugin/
# Validate plugin.json
cat plugins/my-plugin/plugin.json | jq .
# Check error logs
tail -f stockage/logs/error.log
Symptoms: Theme doesn't apply
Check:
theme.json is validSolution:
theme.json syntaxSymptoms: Errors after plugin activation
Solution:
Symptoms: "Cannot write to file" or data loss
Solution:
# Check permissions
chmod 750 stockage/json/
chmod 644 stockage/json/*.json
# Check disk space
df -h
# Verify file locks
lsof stockage/json/
Symptoms: Database errors
Check:
pdo_sqlite extension enabledSolution:
# Check extension
php -m | grep pdo_sqlite
# Check database
sqlite3 stockage/sqlite/flatboard.db "PRAGMA integrity_check;"
# Repair database
sqlite3 stockage/sqlite/flatboard.db ".recover" | sqlite3 recovered.db
Symptoms: No emails received
Check:
Solution:
telnet smtp.gmail.com 587Symptoms: Emails in spam folder
Solution:
Symptoms: "CSRF token mismatch"
Solution:
Symptoms: "Too many requests"
Solution:
Always check logs first:
# Error logs
tail -f stockage/logs/error.log
# PHP errors
tail -f stockage/logs/php-errors.log
# Access logs (if available)
tail -f /var/log/apache2/access.log
Temporarily enable debug mode:
// In config.json or via admin panel
"debug": true
Solution: Clear cache and check autoloader
Solution: Fix file permissions
Solution: Check SQLite extension and permissions
Solution: Update plugin or check compatibility
Last updated: February 23, 2026
]]>Both Flatboard Community and Flatboard Pro are licensed under the GNU General Public License version 3 (GPL3).
What this means:
GPL3 Requirements:
You can:
Note: All usage is free and unrestricted for Community edition. Over 10 years of continuous development, free and regularly updated for the community.
Pricing: €49 (One-time payment, no subscription)
Where to Buy: flatboard.org
Everything in Community Edition, plus:
You can:
Once you purchase and download Flatboard 5 Pro, you have full GPL3 rights to use, modify, and distribute (with GPL3 requirements and the condition that you must have purchased it first).
Each person who distributes Flatboard Pro must have purchased their own copy. This is a condition of distribution, separate from GPL3 requirements.
Flatboard 5 is a complete refactoring of the forum software with a new architecture:
| Feature | Community | Pro |
|---|---|---|
| License | GPL3 | GPL3 |
| Cost | Free (Forever) | €49 (One-time payment) |
| Source Code | Open | Open |
| Storage | ||
| JSON Storage | ✅ | ✅ |
| SQLite Storage | ❌ | ✅ (Better performance) |
| Core Features | ||
| Basic Forum Functionality | ✅ | ✅ (Everything in Community) |
| User Management | ✅ | ✅ |
| Discussion & Post System | ✅ | ✅ |
| Plugins & Themes | ||
| Core Plugins & Themes | ✅ | ✅ |
| Premium Plugin Pack | ❌ | ✅ (Private Messaging, Forum Monitoring, EasyPages, TUI Editor, Storage Migrator, FlatSEO) |
| Premium Themes | ❌ | ✅ |
| API & Development | ||
| API Endpoints | ✅ | ✅ |
| Extended API Features | ❌ | ✅ |
| Support & Updates | ||
| Documentation | ✅ | ✅ |
| Community Support | ✅ | ✅ |
| Priority Support | ❌ | ✅ |
| Feature Updates | ✅ (Regular) | ✅ (3 years guaranteed) |
| Additional | ||
| Advanced Analytics | ❌ | ✅ |
| License Tracking | ❌ | ❌ |
| Subscription Required | ❌ | ❌ (No subscription) |
Community Edition is free and includes JSON storage and core features. Pro Edition costs €49 (one-time) and includes SQLite storage, premium plugins, premium themes, and priority support. Both editions are open source under GPL3.
Yes. GPL3 allows charging for distribution. The source code is still open source and you have full GPL3 rights once you obtain it.
Yes, if you purchased it. GPL3 allows commercial use. You can use Flatboard Pro in any project, commercial or personal, provided you have legally purchased Flatboard Pro first.
Only if you distribute. If you modify Flatboard and distribute it (share with others), you must share your modifications under GPL3. If you only use it internally, you don't need to share.
No. GPL3 is a copyleft license. You cannot remove it or change the license. All derivatives must remain GPL3.
No. Flatboard Pro does not use any license tracking, activation, or validation systems. Once you download it, you have it.
Yes, with conditions. You can distribute Flatboard Pro, but you must:
The full GPL3 license text is available:
LICENSE file in Flatboard 5 source codeCopyright (c) 2015-2026 Frédéric Kaplon and contributors
All Flatboard code is released under the GPL3 license.
This software is provided "as is" without warranty of any kind. Use at your own risk.
Last updated: March 6, 2026
]]>Edit stockage/json/config.json directly (with caution):
{
"custom_setting": "value",
"advanced_option": {
"enabled": true,
"config": {}
}
}
config.json before editing manually. Ensure the forum is in maintenance mode or not running.Use environment variables for sensitive data:
# Set environment variable
export FLATBOARD_DB_PASSWORD="secret"
# Use in PHP
$password = getenv('FLATBOARD_DB_PASSWORD');
Create custom hooks in plugins:
Plugin::hook('custom.event', function($data) {
// Handle custom event
});
# Configuration
php app/Cli/console.php config:get <key>
php app/Cli/console.php config:set <key> <value>
php app/Cli/console.php config:list
# Cache
php app/Cli/console.php cache:clear
php app/Cli/console.php cache:rebuild
# Plugins
php app/Cli/console.php plugin:activate <plugin>
php app/Cli/console.php plugin:deactivate <plugin>
php app/Cli/console.php plugin:list
# Cleanup
php app/Cli/console.php cleanup:cache
php app/Cli/console.php cleanup:empty-discussions
php app/Cli/console.php cleanup:storage
# Database (Pro Edition)
php app/Cli/console.php db:optimize
php app/Cli/console.php db:backup
php app/Cli/console.php db:restore <file>
Create custom CLI commands:
// app/Cli/Commands/CustomCommand.php
namespace App\Cli\Commands;
class CustomCommand
{
public function handle($args)
{
// Command logic
}
}
Check permissions programmatically:
use App\Helpers\PermissionHelper;
if (PermissionHelper::can('create_discussion', $categoryId)) {
// User can create discussion
}
Override permissions for specific users:
// In user profile
$user->setPermissionOverride('create_discussion', true);
Set up webhooks for external services:
Plugin::hook('discussion.created', function($discussion) {
// Send webhook
$payload = json_encode($discussion);
file_get_contents('https://webhook.url', [
'http' => [
'method' => 'POST',
'content' => $payload
]
]);
});
Integrate with external APIs:
$response = file_get_contents('https://api.example.com/data', [
'http' => [
'header' => 'Authorization: Bearer ' . $apiKey
]
]);
Configure custom cache:
use App\Core\Cache;
Cache::set('custom_key', $data, 3600);
$data = Cache::get('custom_key');
Advanced SQLite optimization:
-- Custom indexes
CREATE INDEX idx_custom ON table(column);
-- Query optimization
EXPLAIN QUERY PLAN SELECT * FROM table WHERE column = ?;
-- Statistics
ANALYZE table;
Create custom template overrides:
themes/my-theme/views/
├── discussions/
│ └── custom-view.php
└── layouts/
└── custom-layout.php
Register custom routes:
Plugin::hook('app.routes.register', function($router) {
$router->get('/custom', function() {
return view('custom.template');
});
});
Create custom middleware:
namespace App\Middleware;
use App\Core\Request;
use App\Core\Response;
class CustomMiddleware implements MiddlewareInterface
{
public function handle(Request $request, Response $response): bool
{
// Custom logic — return true to continue, false to stop
return true;
}
}
Add custom security headers:
header('X-Custom-Header', 'value');
header('Content-Security-Policy', "default-src 'self'");
Create custom log entries:
use App\Core\Logger;
Logger::info('Custom event', ['data' => $data]);
Logger::error('Error occurred', ['error' => $error]);
Monitor performance:
$start = microtime(true);
// Code to monitor
$duration = microtime(true) - $start;
Logger::info('Performance', ['duration' => $duration]);
Create custom storage drivers (advanced):
class CustomStorageDriver implements StorageInterface
{
public function save($data) { }
public function load($id) { }
public function delete($id) { }
}
Advanced migration techniques:
// Custom migration script
$oldData = loadFromOldStorage();
$newData = transformData($oldData);
saveToNewStorage($newData);
Add custom user fields:
// In plugin
Plugin::hook('user.profile.fields', function(&$fields) {
$fields[] = [
'name' => 'custom_field',
'label' => 'Custom Field',
'type' => 'text'
];
});
Create custom post types:
Plugin::hook('post.types.register', function(&$types) {
$types['custom'] = [
'name' => 'Custom Post',
'handler' => CustomPostHandler::class
];
});
Send notifications to Discord:
Plugin::hook('discussion.created', function($discussion) {
$webhook = 'https://discord.com/api/webhooks/...';
$payload = [
'content' => "New discussion: {$discussion['title']}"
];
file_get_contents($webhook, [
'http' => [
'method' => 'POST',
'content' => json_encode($payload)
]
]);
});
Integrate with Slack:
$slackWebhook = 'https://hooks.slack.com/services/...';
$message = ['text' => 'Forum notification'];
file_get_contents($slackWebhook, [
'http' => [
'method' => 'POST',
'content' => json_encode($message)
]
]);
Last updated: February 23, 2026
]]>Flatboard 5 is a stable release.
Version numbering:
5.x.y — stable releases5.0.x) contain bug fixes and small improvementsFlatboard 5 is a complete refactoring of the forum software with a new architecture:
Flatboard 5 is available in two editions:
Both editions are open source under GPL3.
Starting with Flatboard 5.2.1, you can update Flatboard directly from the admin panel without FTP or shell access:
admin/updatesstockage/backups/ and can be restored from Admin Panel > Tools > Backups if needed.The update is applied in 5 steps (verify → backup → extract → deploy → cleanup). Protected paths (stockage/, uploads/, .env, .htaccess) are never overwritten during deployment.
For minor version updates (e.g., 5.2.3 to 5.2.4):
stockage/ and uploads/)Creating a Backup Before Update:
backup_YYYY-MM-DD_HH-MM-SS.zip) to a safe location on your computerThis backup can be restored after updating files if needed. See the Backup Guide for more details.
Using Admin Panel (Recommended):
backup_YYYY-MM-DD_HH-MM-SS.zip) to your computerDownload the latest version from the official resources page:
Flatboard_Pro.zipFlatboard_Community.zipReplace application files while preserving your data:
# Backup current installation (if not done via admin panel)
tar -czf backup-$(date +%Y%m%d).tar.gz .
# Extract new version to temporary directory
unzip Flatboard_Pro.zip -d flatboard-new/ # or Flatboard_Community.zip
# Replace files (keep data directories)
cp -r flatboard-new/app/* app/
cp -r flatboard-new/public/* public/
cp -r flatboard-new/themes/* themes/
cp -r flatboard-new/plugins/* plugins/
cp -r flatboard-new/vendor/* vendor/ 2>/dev/null || true
# DO NOT overwrite these directories:
# - stockage/ (contains your data and configuration)
# - uploads/ (contains user uploads)
# Clean up
rm -rf flatboard-new/
After updating, clear the cache:
# Clear cache via CLI
php app/Cli/console.php cache:clear
# Or via Admin Panel
# Go to Admin Panel > Tools > Cache > Clear Cache
Migration tools and detailed procedures will be provided when Flatboard 5 reaches stable release. For now:
Last updated: March 12, 2026
]]>Backups protect against:
Recommended backup schedule:
Backup these directories and files:
Flatboard5/
├── stockage/ # Configuration and data (CRITICAL)
│ ├── json/ # JSON storage files
│ └── sqlite/ # SQLite database (Pro)
├── uploads/ # User uploads (IMPORTANT)
│ ├── avatars/
│ ├── attachments/
│ └── markdown/
├── plugins/ # Installed plugins (OPTIONAL)
├── themes/ # Custom themes (OPTIONAL)
└── languages/ # Custom translations (OPTIONAL)
Must Backup:
stockage/json/ - All JSON files (users, discussions, posts, config)stockage/sqlite/ - SQLite database file (Pro)uploads/ - User-uploaded filesShould Backup:
stockage/logs/ - Log filesOptional:
The easiest way to create backups is using the built-in backup feature in the admin panel:
Navigate to Admin Panel
Create Backup
Download Backup
backup_YYYY-MM-DD_HH-MM-SS.zipYou can also upload a backup file to the server:
Navigate to Admin Panel
Upload Backup
manifest.json file.zip extensionupload_max_filesize / post_max_size limit (displayed on the upload page)Navigate to Admin Panel
Restore Backup
Make sure you have a current backup before restoring an older backup!
# Create backup directory
mkdir -p /backups/flatboard-$(date +%Y%m%d)
# Backup JSON storage
tar -czf /backups/flatboard-$(date +%Y%m%d)/json-backup.tar.gz stockage/json/
# Backup uploads
tar -czf /backups/flatboard-$(date +%Y%m%d)/uploads-backup.tar.gz uploads/
# Backup plugins (if custom)
tar -czf /backups/flatboard-$(date +%Y%m%d)/plugins-backup.tar.gz plugins/
# Backup themes (if custom)
tar -czf /backups/flatboard-$(date +%Y%m%d)/themes-backup.tar.gz themes/
# Backup SQLite database
sqlite3 stockage/sqlite/flatboard.db ".backup /backups/flatboard-$(date +%Y%m%d)/flatboard.db"
# Or copy the file
cp stockage/sqlite/flatboard.db /backups/flatboard-$(date +%Y%m%d)/flatboard.db
# Backup entire installation (excluding core files)
tar -czf /backups/flatboard-full-$(date +%Y%m%d).tar.gz \
stockage/ \
uploads/ \
plugins/ \
themes/ \
languages/
Create automated backup script:
#!/bin/bash
# backup-flatboard.sh
BACKUP_DIR="/backups/flatboard"
DATE=$(date +%Y%m%d-%H%M%S)
BACKUP_PATH="$BACKUP_DIR/$DATE"
# Create backup directory
mkdir -p "$BACKUP_PATH"
# Backup JSON storage
tar -czf "$BACKUP_PATH/json.tar.gz" stockage/json/
# Backup SQLite (if exists)
if [ -f "stockage/sqlite/flatboard.db" ]; then
sqlite3 stockage/sqlite/flatboard.db ".backup $BACKUP_PATH/flatboard.db"
fi
# Backup uploads
tar -czf "$BACKUP_PATH/uploads.tar.gz" uploads/
# Backup custom plugins
tar -czf "$BACKUP_PATH/plugins.tar.gz" plugins/
# Backup custom themes
tar -czf "$BACKUP_PATH/themes.tar.gz" themes/
# Create manifest
echo "Backup created: $DATE" > "$BACKUP_PATH/manifest.txt"
echo "Files:" >> "$BACKUP_PATH/manifest.txt"
ls -lh "$BACKUP_PATH" >> "$BACKUP_PATH/manifest.txt"
# Compress everything
tar -czf "$BACKUP_DIR/flatboard-$DATE.tar.gz" -C "$BACKUP_DIR" "$DATE"
# Remove uncompressed directory
rm -rf "$BACKUP_PATH"
# Keep only last 7 days of backups
find "$BACKUP_DIR" -name "flatboard-*.tar.gz" -mtime +7 -delete
echo "Backup completed: $BACKUP_DIR/flatboard-$DATE.tar.gz"
Make executable:
chmod +x backup-flatboard.sh
Set up automated daily backups:
# Edit crontab
crontab -e
# Add daily backup at 2 AM
0 2 * * * /path/to/backup-flatboard.sh >> /var/log/flatboard-backup.log 2>&1
Store backups on the same server:
Store backups off-site:
# Upload backup via SFTP
scp backup.tar.gz user@backup-server:/backups/
AWS S3:
aws s3 cp backup.tar.gz s3://your-bucket/backups/
Google Cloud Storage:
gsutil cp backup.tar.gz gs://your-bucket/backups/
Dropbox/OneDrive:
Keep multiple backup versions:
# Keep daily backups for 7 days
# Keep weekly backups for 4 weeks
# Keep monthly backups for 12 months
The easiest way to restore a backup is using the built-in restore feature in the admin panel:
If your backup is on your computer and not on the server:
The backup file must:
manifest.json file.zip extensionupload_max_filesize limit)This ensures a clean restore without conflicts. Make sure you have a current backup before restoring an older backup!
The restore process will restore only what was included in the backup:
# Stop forum (put in maintenance mode)
# Extract backup
tar -xzf json-backup.tar.gz
# Restore files
cp -r json/* stockage/json/
# Set permissions
chmod 600 stockage/json/config.json
chmod 644 stockage/json/*.json
# Clear cache
php app/Cli/console.php cache:clear
# Stop forum
# Restore database
sqlite3 stockage/sqlite/flatboard.db < backup.sql
# Or restore from backup file
cp backup.db stockage/sqlite/flatboard.db
# Set permissions
chmod 600 stockage/sqlite/flatboard.db
# Verify integrity
sqlite3 stockage/sqlite/flatboard.db "PRAGMA integrity_check;"
# Extract backup
tar -xzf uploads-backup.tar.gz
# Restore files
cp -r uploads/* uploads/
# Set permissions
chmod -R 750 uploads/
# Extract full backup
tar -xzf flatboard-full-20251225.tar.gz
# Restore directories
cp -r stockage/ stockage/
cp -r uploads/ uploads/
cp -r plugins/ plugins/
cp -r themes/ themes/
# Set permissions
chmod -R 750 stockage/
chmod -R 750 uploads/
chmod 755 plugins/
chmod 755 themes/
# Clear cache
php app/Cli/console.php cache:clear
After restoring:
Flatboard supports a local update workflow for offline or self-hosted deployments where the automatic update check URL is not configured or not reachable. This lets you apply official update archives manually without internet access during the update itself.
.zip) from the official site or your distribution channelUpdate archives are standard ZIP files with an embedded manifest.json that identifies them as Flatboard updates:
{
"type": "update",
"software": "flatboard",
"edition": "pro",
"version": "5.2.1",
"build_date": "2026-03-10",
"checksum": "sha256:...",
"compatible_from": "5.0.0"
}
manifest.json into every Community and Pro archive it produces.Go to Admin Panel → Tools → Backups
Click "Upload Backup"
Select the Flatboard update .zip file from your computer
:::info
The upload page displays the effective PHP upload size limit (upload_max_filesize / post_max_size). The client validates the file size before submitting to avoid a failed round-trip.
:::
Click "Upload" and wait for the upload to complete
The server detects the archive as a Flatboard update (manifest.type == "update" and manifest.software == "flatboard") and routes it to storage/updates/ instead of storage/backups/. A success notice with a link to Admin → Tools → Updates is displayed.
Go to Admin Panel → Tools → Updates
The page scans storage/updates/ and displays one card per valid archive:
| Card style | Meaning |
|---|---|
| Green ("Update") | Archive version is newer than the installed version |
| Yellow ("Reinstall") | Archive version matches the installed version |
| Listed for deletion only | Archive version is older than the installed version |
Click "Update" or "Reinstall" on the desired archive
The 5-step progress UI runs automatically:
| Step | What it does |
|---|---|
| 1 — Verify | Validates manifest (edition, semver ≥ current version, checksum) |
| 2 — Backup | Creates a full automatic backup of your forum before touching any files |
| 3 — Extract | Extracts the archive to a temporary directory |
| 4 — Deploy | Copies new files into the installation, skipping protected paths |
| 5 — Cleanup | Removes the temporary extraction directory |
Each step is executed over a separate CSRF-protected AJAX call with retry logic. The UI shows real-time progress and stops with an error message if any step fails.
The deploy step never overwrites these paths regardless of what the archive contains:
stockage/ — all forum data (JSON / SQLite)uploads/ — user-uploaded files.env — environment configuration.htaccess — web server configurationinstall.php — installerArchives older than the currently installed version are listed separately on the Updates page with a Delete button. They cannot be applied. Clean them up once you no longer need them.
If server is completely lost:
If only some data is lost:
If data is corrupted:
Check:
Check:
Solutions:
Last updated: March 10, 2026
]]>Flatboard 5 follows the Model-View-Controller (MVC) pattern:
app/
├── Controllers/ # Handle requests and logic
├── Models/ # Data models and business logic
├── Views/ # Template files
├── Core/ # Core framework classes
├── Helpers/ # Helper functions
├── Middleware/ # Request middleware
└── Services/ # Service classes
Flatboard5/
├── app/ # Application code
│ ├── Controllers/ # Controllers
│ ├── Models/ # Models
│ ├── Views/ # Views
│ ├── Core/ # Core classes
│ ├── Helpers/ # Helpers
│ ├── Middleware/ # Middleware
│ └── Services/ # Services
├── public/ # Public files
│ └── index.php # Entry point
├── stockage/ # Data storage
├── uploads/ # User uploads
├── plugins/ # Plugins
├── themes/ # Themes
└── vendor/ # Dependencies
PSR-4 autoloading:
use App\Core\Autoloader;
Autoloader::register(BASE_PATH);
Route registration (the Router requires Request and Response instances):
use App\Core\Router;
use App\Core\Request;
use App\Core\Response;
$router = new Router($request, $response);
$router->get('/path', 'Controller@method');
$router->post('/path', 'Controller@method');
// Named routes
$router->get('/forum', 'ForumController@index')->name('forum.index');
$url = $router->url('forum.index'); // Generate URL
$url = $router->url('discussion.show', ['id' => 42]); // With params
// Route groups (shared prefix + middleware)
$router->group(['prefix' => '/admin', 'middleware' => ['App\Middleware\AuthMiddleware']], function($router) {
$router->get('/dashboard', 'Admin\DashboardController@index');
});
// Parameter constraints
$router->get('/user/{id}', 'UserController@show')->where('id', '[0-9]+');
// RESTful resource routes (generates GET list, GET show, POST, PUT, DELETE)
$router->resource('/posts', 'PostController');
// Regex-based routes
$router->regex('GET', '#^/custom/(.+)$#', function($matches) { /* ... */ });
// Global before/after hooks (for monitoring, logging)
$router->beforeEach(function($request) { /* ... */ });
$router->afterEach(function($request, $response) { /* ... */ });
// Route cache for production (cached to stockage/cache/routes.php)
$router->enableCache();
Base controller:
namespace App\Controllers;
use App\Core\Controller;
class MyController extends Controller
{
public function index()
{
return $this->view('template', ['data' => $data]);
}
}
Base model:
namespace App\Models;
class MyModel
{
public static function find($id)
{
// Load from storage
}
public static function create($data)
{
// Create new record
}
}
Follow PSR standards:
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\User;
class UserController extends Controller
{
public function index()
{
$users = User::all();
return $this->view('users.index', ['users' => $users]);
}
public function show($id)
{
$user = User::find($id);
if (!$user) {
return $this->notFound();
}
return $this->view('users.show', ['user' => $user]);
}
}
UserControllergetUserData()$userDataMAX_FILE_SIZEUserController.phpplugins/my-plugin/
├── plugin.json
├── MyPluginPlugin.php
├── assets/
├── views/
└── README.md
<?php
namespace App\Plugins\MyPlugin;
use App\Core\Plugin;
class MyPluginPlugin
{
public function boot()
{
// Register plugin routes
Plugin::hook('router.plugins.register', [$this, 'registerRoutes']);
// Add CSS to page header
Plugin::hook('view.header.styles', [$this, 'addStyles']);
}
public function registerRoutes($router)
{
$router->get('/my-plugin', function() {
return 'Hello from plugin!';
});
}
public function addStyles(&$styles)
{
$styles[] = \App\Helpers\PluginAssetHelper::loadCss('my-plugin', 'css/style.css');
}
}
Below are the most commonly used hooks for plugin development:
| Hook | Use case |
|---|---|
router.plugins.register |
Register plugin routes (preferred for plugins) |
app.routes.register |
Register application-level routes |
view.header.styles |
Inject CSS into <head> |
view.footer.scripts |
Inject JS before </body> |
view.footer.content |
Inject HTML into footer |
view.navbar.items |
Add items to the main navigation bar |
view.admin.sidebar.items |
Add items to the admin sidebar |
admin.dashboard.widgets |
Add widgets to the admin dashboard |
discussion.created |
After a discussion is saved |
post.created |
After a reply is saved |
user.registered |
After a user account is created |
search.results |
Filter or augment search results |
notification.before.create |
Intercept notifications before they are written |
markdown.editor.config |
Modify the Markdown editor configuration |
visitor.page_info |
Resolve page info for unknown URLs (presence) |
presence.users |
Filter/enrich active users list |
themes/my-theme/
├── theme.json
├── assets/
│ ├── css/
│ ├── js/
│ └── img/
└── views/
Override default templates:
themes/my-theme/views/
├── layouts/
│ └── main.php
└── discussions/
└── list.php
Use CSS variables for customization:
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--background-color: #ffffff;
--text-color: #212529;
}
Plugin settings live in the "plugin" section of plugin.json. Always use Plugin::getData/setData/saveData — never Config::get/set — for plugin-specific values:
use App\Core\Plugin;
// Read a setting (third arg is default value)
$apiKey = Plugin::getData('my-plugin', 'api_key', '');
// Dot-notation for nested keys
$host = Plugin::getData('my-plugin', 'smtp.host', 'localhost');
// Write a setting (in-memory only)
Plugin::setData('my-plugin', 'api_key', 'abc123');
// Persist all settings to plugin.json
Plugin::saveData('my-plugin', ['api_key' => 'abc123', 'enabled' => true]);
// Get plugin stats (for monitoring)
$stats = Plugin::getStats();
// Returns: ['total' => int, 'active' => int, 'inactive' => int, 'hooks' => int]
App\Services\PresenceService provides a unified API for querying who is currently on the forum (all methods are static):
use App\Services\PresenceService;
// All presence data (anonymous visitors + bots + logged-in users)
$all = PresenceService::getAllPresence(minutes: 15, includeBots: true);
// Returns: ['visitors' => [...], 'bots' => [...], 'users' => [...], 'all' => [...], 'stats' => [...]]
// Presence on a specific page
$page = PresenceService::getPresenceByPage('/d/123', minutes: 15);
// Aggregate stats only
$stats = PresenceService::getPresenceStats(minutes: 15);
// Returns: ['total' => int, 'anonymous' => int, 'authenticated' => int, 'bots' => int]
// Filter helpers (work on any presence array)
$filtered = PresenceService::filterByPageType($all['all'], 'discussion');
$filtered = PresenceService::filterByCategory($all['all'], 'general');
$filtered = PresenceService::filterByUserGroup($all['users'], 'moderator');
// Sorting
$sorted = PresenceService::sortPresence($all['all'], sortBy: 'last_activity', order: 'desc');
VisitorTrackingMiddleware runs automatically on every non-AJAX HTML request. It skips: authenticated users, paths under /api/, /presence/update, /favicon.ico, /robots.txt, static file extensions. It fires visitor.before_track before writing each record.
// Both are equivalent
$text = Translator::trans('key', ['var' => 'value'], 'domain');
$text = __('key', ['var' => 'value'], 'domain');
// Get current language code
$lang = Translator::getLanguage(); // e.g., 'fr', 'en'
// Change language for the current request
Translator::setLanguage('en');
// Reload all translations from disk
Translator::reload();
// Reload only theme translation overrides
Translator::reloadThemeTranslations();
// Get all keys for a domain (useful for debugging)
$all = Translator::getAll('main');
// Register plugin translations programmatically
Translator::addPluginTranslations('my-plugin', ['key' => 'value']);
Working with JSON storage:
use App\Core\AtomicFileHelper;
// Read (returns array or null if file absent)
$data = AtomicFileHelper::readAtomic('path/to/file.json');
// Write (returns bool)
AtomicFileHelper::writeAtomic('path/to/file.json', $data);
// Batch read multiple files in one pass
$results = AtomicFileHelper::readAtomicBatch([
'path/to/file1.json',
'path/to/file2.json',
]);
Working with SQLite:
use App\Storage\SqliteStorage;
$storage = new SqliteStorage();
$result = $storage->query('SELECT * FROM users WHERE id = ?', [$id]);
Always validate input:
use App\Core\Validator;
$validator = new Validator();
$validator->required('email')->email('email');
if (!$validator->validate($data)) {
return $this->error($validator->errors());
}
Sanitize all output:
use App\Core\Sanitizer;
$clean = Sanitizer::clean($userInput);
echo htmlspecialchars($clean, ENT_QUOTES, 'UTF-8');
Use CSRF tokens:
use App\Core\Csrf;
// Generate token
$token = Csrf::token();
// Verify token
if (!Csrf::verify($token)) {
return $this->error('Invalid CSRF token');
}
Write unit tests:
use PHPUnit\Framework\TestCase;
class UserTest extends TestCase
{
public function testUserCreation()
{
$user = User::create([
'username' => 'testuser',
'email' => '[email protected]'
]);
$this->assertNotNull($user);
}
}
Test integrations:
public function testApiEndpoint()
{
$response = $this->get('/api/v1/discussions');
$this->assertEquals(200, $response->getStatusCode());
}
Use caching:
use App\Core\Cache;
// Set cache
Cache::set('key', $data, 3600);
// Get cache
$data = Cache::get('key');
// Clear cache
Cache::clear('key');
Optimize queries:
// Use indexes
// Limit results
// Avoid N+1 queries
// Use transactions
When developing plugins, themes, or customizations:
Last updated: February 23, 2026
]]>