Install Banimark on plain PHP
Run a self-hosted AI support desk on any PHP site: drop one file in your web root, open the browser installer, embed the widget. Works with vanilla PHP, WordPress and legacy apps.
What you are about to do
- Check the server has PHP and the ionCube loader
- Get the package - with Composer, or by downloading one zip
- Create one file in your web root
- Open it in a browser and follow the installer
- Paste one line into your template
You do this once. From then on, new versions install themselves from a button in your own Banimark admin — no terminal, no developer, and a way back if you need one. How updating works.
No framework, no build step, no router to configure. Banimark's standalone runtime is a single entry file that serves the admin panel, the widget and the chat endpoint from one path — which is why the same instructions cover vanilla PHP, an old in-house app, and WordPress.
Before you start
| Needs | Why |
|---|---|
| PHP 8.2 or newer | The engine ships encoded for PHP 8.2, and an 8.2 build runs on 8.2 and later — it will not run on 8.1 at all. We test on 8.2 and 8.3. |
| ext-curl, ext-json, PDO plus a driver: pdo_mysql, pdo_pgsql or pdo_sqlite | Calling your AI provider, and talking to your database. |
| The ionCube loader | The engine ships encoded, so the free loader has to be active. Without it, Banimark shows a page saying exactly what to install — the rest of your site keeps working. How to install it. |
| A database MySQL 5.7+/8, MariaDB 10.4+, PostgreSQL 12+, or SQLite | Banimark keeps its own banimark_* tables. It never writes to yours. |
| Composer | How the package is installed. Banimark is not on Packagist; each guide shows the one line that tells Composer where it lives. |
| HTTPS (strongly recommended) | Chat transcripts and the signed visitor token travel over it. |
No queue worker, no Redis, no Node build step, no cron job required.
Install the ionCube loader
Your licensed build of Banimark ships encoded, so the ionCube PHP loader has to be active before anything will run. Most commercial hosts already have it — check first:
php -v
# ...with the ionCube PHP Loader <- you are done, skip this section
If that line is there, skip the rest of this section entirely. If it is missing, the setup script below does the whole job for you.
The quick way: run the setup script
One command checks everything Banimark needs — PHP version, extensions, the ionCube loader, Composer, whether this server can reach us — and installs the loader if it is missing. It works out your operating system, processor, PHP version and control panel by itself, and asks before it changes anything.
Look before you leap. Run it with --check first: it reports exactly what it
finds and what it would do, and changes nothing at all. Safe on a live server.
curl -fsSL https://banimark.com/setup.sh | bash -s -- --check
Happy with what it says? Run it for real:
curl -fsSL https://banimark.com/setup.sh | bash
irm https://banimark.com/setup.ps1 | iex
What it does, in order:
- Finds every PHP on the server and asks which one your website uses — on cPanel, Plesk, XAMPP or a Mac with MAMP there is usually more than one.
- Checks PHP 8.2+, the
json,curl,pdo,mbstringandsodiumextensions, a database driver, andzip(for one-click updates). Where it can, it offers to install or switch on what is missing. - Downloads the official loader from ionCube for your exact PHP, copies it in, and adds it to PHP's configuration so it loads first — backing up any file it changes. It then starts PHP to prove it works, and puts everything back if it does not.
- Offers to restart PHP-FPM / Apache / LiteSpeed, and to check the PHP your website actually runs (a tiny, randomly named file in your public folder, read once and deleted).
The script is plain text — read it (or the Windows one) before you run it. On shared hosting, where nobody can change PHP from a terminal, it tells you where the switch is in your panel instead.
Prefer to do it by hand, or need to send it to whoever runs your server? The manual steps:
How to install the ionCube loader
The loader is a single .so that must match your exact PHP minor version
— the current archive carries builds for PHP 8.1 through 8.5.
On your own Linux server
# 1. which PHP, and where do its extensions live?
php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION."\n";' # e.g. 8.3
php -r 'echo ini_get("extension_dir")."\n";' # e.g. /usr/lib/php/20230831
# 2. fetch the loaders (use ioncube_loaders_lin_aarch64.tar.gz on ARM servers)
cd /tmp
curl -fsSLO https://downloads.ioncube.com/loader_downloads/ioncube_loaders_lin_x86-64.tar.gz
tar xzf ioncube_loaders_lin_x86-64.tar.gz
# 3. copy the ONE that matches your PHP
PHPMM=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
sudo cp "ioncube/ioncube_loader_lin_${PHPMM}.so" "$(php -r 'echo ini_get("extension_dir");')/"
Now switch it on. On Debian and Ubuntu, PHP reads a directory of .ini files, and the
loader has to be first — the 00- prefix is what guarantees it loads before OPcache:
PHPMM=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
echo "zend_extension=ioncube_loader_lin_${PHPMM}.so" | sudo tee /etc/php/${PHPMM}/mods-available/00-ioncube.ini
sudo ln -sf /etc/php/${PHPMM}/mods-available/00-ioncube.ini /etc/php/${PHPMM}/fpm/conf.d/00-ioncube.ini
sudo ln -sf /etc/php/${PHPMM}/mods-available/00-ioncube.ini /etc/php/${PHPMM}/cli/conf.d/00-ioncube.ini
sudo systemctl restart php${PHPMM}-fpm
php -v # must now say "with the ionCube PHP Loader"
On RHEL, Rocky and AlmaLinux the equivalent file is /etc/php.d/00-ioncube.ini,
then sudo systemctl restart php-fpm.
On cPanel, Plesk or shared hosting
- cPanel — Select PHP Version → Extensions, tick
ioncube_loader. From a WHM shell, find EasyApache's package for your PHP withdnf list available 'ea-php83-php-ioncube*'and install the newest one — Banimark needs loader 15 or newer. The setup script does this for you. - Plesk — the loader ships with Plesk's own PHP builds; enable it under Websites & Domains → PHP Settings.
- Anything else — ask your host to “enable the ionCube PHP loader”. It is a standard request and almost every commercial host supports it.
How you will know
You do not have to guess. If the loader is missing, or PHP has not been restarted since you added
it, every Banimark admin page shows “Banimark needs the ionCube Loader” with these
steps, the chat widget politely says support is temporarily unavailable, and the rest of your
site carries on as normal. On Laravel, php artisan banimark:doctor says the same thing
in the terminal. Once it is fixed, reload — nothing needs reinstalling.
Get the package
Two ways, and they end in the same place. Use Composer if your project already has it; if it does not — common on shared hosting — download one zip instead. Banimark needs no other packages, so nothing is lost either way.
With Composer
From the root of your project — wherever your vendor/ directory lives, or
should live:
composer require "banimark/banimark:^0.32"
Without Composer
Put Banimark in a folder called banimark beside your web root, not
inside it — on most hosts that is your home directory, next to public_html. From
a terminal, in that directory:
curl -fsSLo banimark.zip https://github.com/trimmytech/banimark-dist/archive/refs/tags/v0.32.1.zip
unzip -q banimark.zip && mv banimark-dist-0.32.1 banimark && rm banimark.zip
No terminal? Download
banimark 0.32.1 (.zip), upload it with your
host's file manager into the folder above your web root, extract it there and rename the
folder it creates, banimark-dist-0.32.1, to banimark.
Create one entry file
Put this in your web root. It is the entire application: every Banimark URL is this file plus a path. Which one depends on how you got the package:
<?php
require __DIR__.'/../vendor/autoload.php';
\Banimark\Standalone\App::run(__DIR__.'/../banimark.config.php');
<?php
require __DIR__.'/../banimark/autoload.php';
\Banimark\Standalone\App::run(__DIR__.'/../banimark.config.php');
Run the browser installer
Open /banimark.php/install. It checks the server line by line
(PHP version, extensions, PDO drivers, ionCube, whether the config directory is writable),
then asks for your database, your admin account and — optionally — your first AI
provider. When it finishes it writes the config and locks itself; visiting
/install again does nothing.
| URL | What it is |
|---|---|
/banimark.php/admin | the admin panel |
/banimark.php/widget.js | the chat widget |
/banimark.php/chat | the chat endpoint the widget posts to |
/banimark.php/chat-page | the chat as a shareable full page |
Hosts that strip PATH_INFO are handled too: every route also answers
as ?r=/admin.
Prefer clean addresses such as /support, or the panel somewhere private? See
Your own addresses below.
Embed the widget
One line before </body>, on any page where you want the launcher:
<script src="/banimark.php/widget.js" defer></script>
Let the AI answer about the logged-in customer
Anonymous visitors get general answers. To let the assistant look up this customer's orders, tickets or invoices, your PHP app mints a short-lived signed token and the widget carries it. The token is made on your server with a shared secret; the AI never sees it, and it cannot choose whose records to read — the scoping comes from the claims you signed.
An invalid or expired token quietly means “anonymous”. It is never an error a visitor can probe.
Read the secret the installer generated, then sign the claims you want scoped:
<?php
require __DIR__.'/../vendor/autoload.php';
$cfg = require __DIR__.'/../banimark.config.php';
$pdo = new PDO($cfg['dsn'], $cfg['db_user'], $cfg['db_pass']);
$secret = (new \Banimark\Standalone\Settings($pdo))->get('identity_secret');
$token = \Banimark\Identity\VisitorToken::mint(
['user_id' => $_SESSION['user_id'], 'name' => $_SESSION['user_name']],
$secret
);
<script src="/banimark.php/widget.js"
data-token="<?= htmlspecialchars($token, ENT_QUOTES) ?>" defer></script>
Your own addresses (optional)
Out of the box everything lives under /banimark.php. You can give Banimark clean
addresses — for example yoursite.com/support for the widget and the chat link
— and move the admin panel somewhere only your team knows. Nothing inside Banimark needs
changing for the first part: it notices the address your visitors used and builds every link
from it.
Apache, LiteSpeed and cPanel hosting (.htaccess)
Put this in the .htaccess file in the same folder as banimark.php
(create it if it is not there). Change support to any word you like.
# Banimark at yoursite.com/support instead of yoursite.com/banimark.php
RewriteEngine On
RewriteRule ^support/?(.*)$ banimark.php?r=/$1 [L,QSA]
Your addresses are then:
| Before | After |
|---|---|
/banimark.php/widget.js | /support/widget.js |
/banimark.php/chat-page | /support/chat-page |
/banimark.php/admin | /support/admin |
Use the new widget address in your pages:
<script src="/support/widget.js" defer></script>. The old
/banimark.php/... addresses keep working, so nothing breaks while you switch.
nginx
nginx does not read .htaccess. Add this inside your site's server { }
block, then reload nginx:
location ^~ /support {
rewrite ^/support/?(.*)$ /banimark.php?r=/$1 last;
}
Move the admin panel
To have the panel at /support/control instead of /support/admin, add
one line to banimark.config.php (next to your database settings):
'admin_path' => 'control',
The old /admin address then answers "nothing here", so it does not point anyone to
the panel. Pick any plain word (letters, numbers, - or _); the
addresses the visitor side uses — chat, widget.js,
upload, file, install — cannot be taken. Moving the
panel is a nice extra, not a lock: your team's sign-in (and two-factor codes) is what protects
it.
If links ever come out wrong behind an unusual proxy, say the address outright with
'public_path' => '/support', in the same file.
The mobile app
Point the Flutter SDK at the new address:
BanimarkConfig.standalone('https://yoursite.com/support').
WordPress and other CMSs
Exactly the same file, dropped in the WordPress root beside wp-config.php, with the
script tag added through your theme's wp_footer hook. Banimark never loads
WordPress and WordPress never loads Banimark — they share a server, not a process, so an
upgrade on either side cannot break the other.
Updating
The short version: two buttons, no terminal
Open /banimark.php/admin, go to Changelog, and when a new release exists
you get a button that says Update to …. Press it. When it finishes, press
Update the database. That is the whole thing, and nobody needs a command line, SSH or a
developer to do it.
What happens when you press it:
- Banimark downloads the release from us, using your licence key.
- It checks the download is byte-for-byte the file we signed. If the signature or the hash does not match, it is thrown away and nothing on your server changes.
- It unpacks the new version beside the current one — never over it.
- It renames your current version aside as a backup, and moves the new one into place. If anything goes wrong at that step, the backup goes straight back.
- Your previous version stays on the server — just the one, so updates never pile copies onto your disk — and Previous versions offers a Restore this one button for it.
The same page also has, whenever they apply:
- Check for updates now — on every state of the page. Banimark remembers our answer for a few hours; this asks again straight away.
- Reinstall — downloads the version you are on and puts a fresh copy in place, for when files have been damaged or edited by mistake.
If the button is not there
The Changelog page will say which of these it is, in plain words:
- Your licence has lapsed. You can still see what is new — downloading the file is what the licence pays for. Renew and the button comes back.
- The server will not let PHP write to its own folder. Common on shared hosting. The page names the exact directory to give the web server write access to; your host can do it in a minute. Until then, use the command line below.
- This release has to be installed by hand. Some releases need a step we will not do behind your back — a newer PHP, say. We hold those back from the button deliberately.
The command line, when you want it
Nothing about the button replaces this, and a developer updating from a terminal changes nothing about how the panel behaves. Name the release line you are moving to:
composer require "banimark/banimark:^0.32"
Why not composer update? Banimark is still on 0.x, where Composer treats every
minor release as a breaking one: ^0.25 will never install 0.26. composer update
picks up fixes within your line; composer require with the new line is how you move
up. The newest line right now is ^0.32.
The database changes, and what applies them
In Laravel, a release that needs new columns ships a migration and
php artisan migrate applies it. You have no migration runner, so Banimark carries the
same job itself: one function, Schema::ensureCurrent(), which brings an existing
database up to whatever the installed version expects. It is what the panel button calls, what the
Laravel migration calls, and what you can call yourself.
It is version-gated. It compares the schema_version row in
banimark_settings with the version of the code now on disk, does the work once if they
differ, and is a no-op every time after that. The chat endpoint never runs it, so your visitors
never pay for it.
If you updated from the command line instead of the panel, signing in to
/banimark.php/admin applies it too.
The deploy-script way: do it yourself
If you deploy from CI, or you would rather not have the first staff visit of the day be the
migration, call it directly. Put this beside banimark.config.php:
<?php
require __DIR__.'/vendor/autoload.php';
$cfg = require __DIR__.'/banimark.config.php';
$pdo = new PDO($cfg['dsn'], $cfg['db_user'], $cfg['db_pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$changed = \Banimark\Storage\Schema::ensureCurrent(
$pdo,
\Banimark\Licensing\Master::PACKAGE_VERSION
);
echo $changed
? "Banimark schema brought up to date.\n"
: "Nothing to do.\n";
composer require "banimark/banimark:^0.32"
php banimark-upgrade.php
Safe to run on every deploy, in any order relative to your own migrations. On a database that was never installed it does nothing at all and says so — it upgrades an install, it does not create one. Run the browser installer for that.
What it actually does to your database
CREATE TABLE IF NOT EXISTSfor any table a new release added.ALTER TABLE … ADD COLUMNfor any column a new release added, swallowing the duplicate error so it is safe to re-run.- Creates any missing index.
- Converts Banimark's own tables to
utf8mb4if they are not already — the reason emoji work.
It is additive. Nothing is dropped, nothing is rewritten, and it only ever touches tables with Banimark's own prefix. Your application's schema is not involved.
Check that it worked
This matters more than it sounds, because of the next section. Ask the database what version it thinks it is on:
SELECT `value` FROM banimark_settings WHERE `key` = 'schema_version';
It must match the version shown on the panel's Changelog page. If it lags behind, the upgrade has not run.
If the version never moves
Almost always one thing: the database user Banimark connects as can read and write rows but cannot
change the schema. ensureCurrent() is deliberately built never to throw — a panel
that cannot write a settings row must still open — so a missing privilege looks exactly like
nothing happening. No error, no warning, just an old version number.
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX
ON banimark.* TO 'banimark'@'localhost';
FLUSH PRIVILEGES;
Then run the upgrade again. Those seven are enough for both the first install and every upgrade
after it; Banimark needs no DROP and no REFERENCES.
Updating without Composer
Use the button in the panel — with no Composer there is no composer.lock to fall out
of step, so it is the natural way. It replaces the banimark folder in one step and keeps
the previous one beside it, so the folder above your web root must be writable by PHP; the
Changelog page checks that for you and says so if it is not.
By hand instead: download the new release's zip, extract it, and swap it in for the
banimark folder — your banimark.config.php lives outside it and is
untouched. Then sign in to the panel once, which brings the database up to date.
Your licence
A fresh install asks us for a free trial on its own — 7 days, one per site . When it ends, the admin panel locks until you enter a purchased key. Your chat widget is never affected: visitors carry on being served whatever your licence says.
Enter the key under License in the panel, or set it before first boot:
BANIMARK_LICENSE_KEY=BM-XXXX-XXXX-XXXX-XXXX
Activation needs one call to our servers. After that the licence is good offline for 14 days at a time, so a network problem on your side never interrupts anything.
First five minutes in the panel
- AI Providers — add your own key. Gemini, OpenAI, Anthropic, DeepSeek, Groq, Mistral, OpenRouter, xAI, or a local Ollama / LM Studio through a base URL.
- Rules — plain sentences in folders, not prompt engineering. “Never promise a refund date.” Switch a folder off without deleting it.
- Tools — the read-only lookups the assistant may run against your database. Pick a table, tick the columns it may show, and scope every row to the signed-in visitor. SELECT only, always bound, never widened by the AI.
- Staff — invite your team. Banimark has its own logins, separate from your application's users, with per-person permissions and optional 2FA.
- Notifications — SMTP, so a handover reaches someone.
Check it over
Before you call it done:
- Open the panel and confirm the licence page is green.
- Send a message through the widget on a real page — not just curl. The widget is JavaScript; a curl check proves nothing about it.
- Type an emoji. If it comes back, your database is
utf8mb4and you are fine. - Ask something that needs a tool, while signed in, and confirm it returns your rows.
- Trigger a handover and confirm the email lands.
Not your stack?
Laravel
A first-class package: one composer require, one artisan command, done.
Read the guide →CodeIgniter
The standalone runtime inside CodeIgniter 4's public folder, with the token minted from your session.
Read the guide →Node.js
Banimark is PHP. Run it beside your Node app and mount it on the same origin with nginx.
Read the guide →Python
Same shape as Node: a small PHP service behind your Django, Flask or FastAPI app.
Read the guide →Docker
No PHP on the server? Bring your own: PHP-FPM, nginx and MySQL in three containers.
Read the guide →