- Backend: PHP 8.1+ (no framework, custom MVC in
src/) - Database: MySQL 8.0 (NOT SQLite) — all queries use MySQL syntax
- Frontend: Bootstrap 5, vanilla JS, server-rendered PHP views
- Agent: Python 3 (
agent/bbs-agent.py) — runs on client machines, polls server for tasks - OS: Ubuntu Server 22.04+ only — no other distros supported
src/Controllers/— route handlers (extendsrc/Core/Controller.php)src/Services/— business logic (PluginManager, QueueManager, etc.)src/Views/— PHP templates (Bootstrap)src/Core/App.php— routes,Controller.php— base controller,Database.php— MySQL PDO wrapperagent/bbs-agent.py— client agentschema.sql— consolidated DB schema (source of truth for fresh installs)migrations/— incremental SQL migrationsbin/bbs-install— installer,bin/bbs-update— updaterscheduler.php— cron-driven task runner
- Server version:
VERSIONfile in project root. - Agent version:
AGENT_VERSIONconstant inagent/bbs-agent.py. - Always synchronized. As of
2.53.0the agent and server share the same version number. When cutting a release, bump both to the same value, regardless of whether the agent code changed.
Software is installed at /var/www/bbs/ on all servers. Config lives at /var/www/bbs/config/.env.
- Host:
ssh bbs@falcon.borgbackupserver.com— bare VM install, primary test/dev server - Sudo: passwordless sudo available for troubleshooting
- MySQL access:
sudo mysql(no password needed via sudo) - Deploy dev code:
sudo /var/www/bbs/bin/bbs-update /var/www/bbs main - Uses Apache mod_php (not PHP-FPM), so
fastcgi_finish_request()is unavailable
- Host:
ssh bbs@us.borgbackupserver.com— Docker host for tenant provisioning (NOT a BBS install, do not deploy code here) - Demo Tenant:
https://demo.us.borgbackupserver.com - Infrastructure code:
~/Projects/bbs-docker-hosting/(separate from this repo, not public)
- Scooter:
ssh root@scooter.falconinternet.net— test client for agent installs and backup testing
NEVER use sudo directly in PHP code. Production servers do NOT have passwordless sudo configured for www-data.
The ONLY way to execute privileged operations is through bin/bbs-ssh-helper:
- All privileged commands MUST go through the SSH helper
- The SSH helper is the ONLY binary with sudoers access for www-data
- Direct
sudocalls will FAIL silently or error on production systems
Allowed pattern:
$helper = __DIR__ . '/../bin/bbs-ssh-helper';
$cmd = "sudo $helper <command> <args>";FORBIDDEN patterns:
// NEVER DO THIS - will fail on production!
shell_exec("sudo /some/command");
shell_exec("sudo -u someuser /bin/something");If you need a new privileged operation, ADD IT TO bin/bbs-ssh-helper first.
- Repo:
marcpope/borgbackupserveron GitHub - Commit and push: Commit to
main, push to origin. Always use descriptive commit messages. NEVER addCo-Authored-Bytrailers or any other attribution to Claude in commit messages. - Releases: Tagged as
vX.Y.Z(e.g.,v1.2.0). To create a release:- Bump
VERSIONfile (only bumpAGENT_VERSIONinagent/bbs-agent.pyif agent code changed) - Commit the version bump
- Tag:
git tag vX.Y.Z - Push:
git push origin main --tags - Create a GitHub Release (required for the update checker to see it):
gh release create vX.Y.Z --title "vX.Y.Z" --notes "Release notes here"The server's "Check for Updates" uses the GitHub Releases API — tags alone are NOT sufficient. - Production servers pull the latest tag via
bbs-update(without themainargument)
- Bump
- Auth:
$this->requireAuth(),$this->verifyCsrf(), agent ownership check - Flash + redirect:
$this->flash('success', 'msg'); $this->redirect('/path'); - JSON response:
$this->json(['status' => 'ok', ...]) - DB queries:
$this->db->fetchOne(),$this->db->fetchAll(),$this->db->insert(),$this->db->update(),$this->db->delete() - Job queue:
backup_jobstable — statuses: queued → sent → running → completed/failed - Agent communication: Agent polls
GET /api/agent/tasks, reports viaPOST /api/agent/status
pluginstable — master plugin list (slug-based dispatch)agent_plugins— per-agent enable/disableplugin_configs— named reusable configurations (e.g., "Production DB")backup_plan_plugins— links plans to plugin configs viaplugin_config_id- Plugin UI lives on the Plugins tab of client detail page
- Schema-driven forms via
PluginManager::getPluginSchema()