- Troubleshooting
- Support
- Installation & Setup
- Configuration
- Echo Areas
- Netmail
- Binkp Server & Polling
- Users & Authentication
- Maintenance
- File Areas
- Multi-Network Support
- LovlyNet Network
- Database
- WebDoors
- General
Q: The page looks broken after an upgrade — missing features, broken menus, or "loadI18nNamespaces is not defined" errors
A: This is a stale service worker cache issue. The old service worker is still serving cached JavaScript and CSS from before the upgrade. You need to unregister the service worker so the browser fetches fresh files.
Desktop browsers (Chrome / Edge)
- Open DevTools — press
F12or right-click → Inspect - Go to Application → Service Workers
- Click Unregister next to the BinktermPHP service worker
- Reload the page (
F5)
Desktop browsers (Firefox)
- Open
about:debugging#/runtime/this-firefoxin the address bar - Find the BinktermPHP worker and click Unregister
- Reload the page
Desktop — quick alternative (all browsers)
A hard refresh bypasses the cache without unregistering the service worker:
- Windows/Linux:
Ctrl + Shift + R - Mac:
Cmd + Shift + R
Mobile (Chrome on Android)
- Open Chrome's menu (three dots) → Settings → Privacy and security → Clear browsing data
- Select Cached images and files and Cookies and site data for the BinktermPHP site
- Tap Clear data, then reload
Mobile (Safari on iOS)
- Go to Settings → Safari → Clear History and Website Data
- Reload the BinktermPHP site
Note: After clearing, you will be logged out and will need to sign in again. This is normal.
A: Install lhasa.
On Debian/Ubuntu:
apt-get install lhasaA: There are several ways to get help with BinktermPHP:
- Discussions: For general questions, help, and community support, visit the Claude's BBS or GitHub Discussions
- Bug Reports: If you've found a bug, please file an issue at GitHub Issues
- Feature Requests: Have an idea for a new feature? Submit it at GitHub Issues
A: BinktermPHP requires:
- PHP 8.1 or higher
- PostgreSQL 12 or higher
- Composer for dependency management
- A web server (Apache, Nginx, etc.)
- Operating System: Linux/UNIX is recommended. MacOS and *Windows should also work
A: Edit .env and configure the database settings:
DB_HOST=localhost
DB_PORT=5432
DB_NAME=binktest
DB_USER=your_username
DB_PASS=your_password
Use .env.example as a reference or copy it over to .env to get started.
A: The data/outbound directory needs special permissions for the mailer to work:
chmod a+rwxt data/outboundThis allows the web server and CLI scripts to create and manage outbound packets.
A: Run the migration script:
php scripts/setup.phpThis will apply all pending migrations from the database/migrations/ directory.
A: The simplest approach is to copy the entire BinktermPHP directory to the new server along with a database dump.
Step 1 — Dump the database on the old system
pg_dump -U your_db_user your_db_name > binkterm_backup.sqlStep 2 — Stop the daemons on the old system
Stop the BinkP server, admin daemon, and any other BinktermPHP processes before copying to avoid transferring files mid-write.
Step 3 — Copy the directory to the new system
Copy the entire BinktermPHP directory including the dump file:
rsync -av /path/to/binkterm-php/ newserver:/path/to/binkterm-php/Or use scp, a tarball, or whatever transfer method suits your setup.
Step 4 — Create the database and user on the new system
sudo -u postgres psqlCREATE USER your_db_user WITH PASSWORD 'your_password';
CREATE DATABASE your_db_name OWNER your_db_user;
GRANT ALL PRIVILEGES ON DATABASE your_db_name TO your_db_user;
\qStep 5 — Restore the database
Run the restore as the database user you just created (the same user BinktermPHP connects as):
psql -U your_db_user -d your_db_name < binkterm_backup.sqlStep 6 — Update .env for the new environment
Edit .env on the new system and update anything that is host-specific:
DB_HOST,DB_PORT,DB_NAME,DB_USER,DB_PASS— if the database details differ on the new serverSITE_URL— update to the new hostname or URL
Step 7 — Run setup
php scripts/setup.phpThis ensures file permissions are correct and applies any pending migrations.
Step 8 — Start the daemons
Start the BinkP server, admin daemon, and any other services on the new system.
Notes:
vendor/does not need to be reinstalled — Composer's autoloader uses relative paths and works correctly regardless of where the installation directory lives on the filesystem.- If the new system has a different public hostname or IP address, update your LovlyNet registration (
php scripts/lovlynet_setup.php --update) and notify any downlinks of the change. - Verify the web server on the new system points to
public_html/as its document root.
A: The safest method is to do a fresh git clone and then copy your local runtime and configuration files into it.
Recommended steps:
- Back up your current installation, especially
.env,data/, and any local customizations. - Clone the repository into a new directory:
git clone https://github.com/awehttam/binkterm-php.git- Copy these from the old installation into the new clone:
.envdata/- any intentional custom templates or local files
- Install dependencies if needed:
composer install- Run setup:
php scripts/setup.php- Restart the daemons and web services.
This is preferred over converting the existing ZIP-based directory in place,
because ZIP installs usually do not contain .git history and may already
have local file changes that are hard to reconcile cleanly.
If you must convert the existing directory in place, you can initialize git, add the remote, fetch, and check out the branch:
git init
git remote add origin https://github.com/awehttam/binkterm-php.git
git fetch origin
git checkout -b main origin/mainThat approach is riskier and more likely to leave you with a dirty tree or conflicts if the ZIP-installed files differ from the branch you are checking out. After converting in place, run:
composer install
php scripts/setup.phpA: You can do this in place by unzipping the ZIP release over the existing
git-based installation and then deleting the .git directory. That is the
quickest method and it will usually preserve your existing vendor/
dependencies.
Quick in-place method:
- Back up
.env,data/, and any local custom files. - Unzip the ZIP release over the existing git-based installation.
- Delete the
.git/directory. - Run:
php scripts/setup.php- Restart the daemons and web services.
If vendor/ is missing, dependencies changed, or you want to ensure the
installed packages match composer.lock, also run:
composer installThe safer method is to unpack the ZIP release into a new directory and then copy your local runtime and configuration files into it.
Recommended steps:
- Back up your current installation, especially
.env,data/, and any local customizations. - Download and extract the BinktermPHP ZIP release into a new directory.
- Copy these from the git-based installation into the new ZIP-based install:
.envdata/- any intentional custom templates or local files
- If the resulting install does not already have a working
vendor/directory, run:
composer install- Run setup:
php scripts/setup.php- Restart the daemons and web services.
Do not copy the old .git/ directory into the ZIP-based install. If you later
decide to return to git-based updates, it is better to clone the repository
fresh and then copy .env and data/ back into that clone.
A: The binkp mailer is configured in data/binkp.json. This file contains:
- System information (name, address, sysop, location)
- Binkp server settings (port, timeout, max connections)
- Uplink configurations (addresses, passwords, domains)
- Security settings
A: Use the BBS Settings page from the Admin Menu
A: SITE_URL in .env is used for generating full URLs (share links, password reset emails, etc.). This is important if your server is behind a reverse proxy or load balancer where $_SERVER['HTTPS'] may not be set correctly.
A: Copy the example themes configuration and customize it:
cp config/themes.json.example config/themes.jsonThen edit config/themes.json to add or modify available themes:
{
"Amber": "/css/amber.css",
"Cyberpunk": "/css/cyberpunk.css",
"Dark": "/css/dark.css",
"Green Term": "/css/greenterm.css",
"Regular": "/css/style.css",
"My Custom Theme": "/css/mycustom.css"
}Key points:
- The key is the display name shown to users in their settings
- The value is the path to the CSS file (relative to
public_html/) - CSS files must be placed in
public_html/css/ - Users can select themes from their account settings page
- The "Regular" theme should always point to
/css/style.css(the base theme)
Creating a new theme:
- Copy an existing theme CSS file (e.g.,
public_html/css/style.css) - Rename it to your theme name (e.g.,
mytheme.css) - Customize the colors and styles
- Add an entry to
config/themes.json
Users will see the new theme in their settings dropdown immediately after the config file is updated.
A: When creating or editing an echo area, check the "Local Only" checkbox and set the domain to "local". Local echo areas:
- Store messages in the database normally
- Do NOT transmit messages to uplinks
- Still require a domain to be set (use a name like "local")
- Use the system address for message origin
A: The dashboard echomail count now shows the number of new messages since your last visit to echomail. Visiting an echomail page resets that count, so it can show 0 even when there are older messages you have not opened.
You can change the badge back to total unread messages in your settings, but that count can take longer to display on systems with a large message base.
A: This error occurs when posting to an echo area whose domain has no configured uplink. Solutions:
- For local-only areas: Enable the "Local Only" flag on the echo area
- For networked areas: Add an uplink configuration in
binkp.jsonfor that domain
A: It is a per-echo-area override for the outbound packet destination. For most installations it should be left blank.
When BinktermPHP spools an outbound echomail message it determines where to send it using this priority order:
- Echo area
uplink_address— if set, this address is used for that area only - Domain-level uplink — the uplink in
binkp.jsonwhosedomainmatches the echo area's domain - Fallback — the uplink in
binkp.jsonmarked"default": true, or if none is marked, the first enabled uplink - None — message is not sent upstream (effectively local delivery)
In practice, for a typical setup, step 2 handles everything and uplink_address is always left blank. It would only be needed if you had two different uplinks on the same domain and wanted specific echo areas routed to a specific one — an uncommon scenario.
Note: The uplink_address stored here is just the FTN address (e.g. 21:1/100). The actual connection details (hostname, port, password) are always defined in binkp.json — this field only selects which of those configured uplinks to use.
A:
- Make sure you're using a monospace font such as Courier new. Non mono-space fonts will not render ANSI correctly.
A: This is standard Markdown behaviour. A single newline in the source text is treated as a space — not a line break. This matches the CommonMark spec.
To produce a line break in Markdown, use one of:
- Two spaces at the end of the first line, followed by a newline
- A blank line between the two lines (produces a paragraph break)
If the message was written in a plain-text mailer or editor that uses single newlines, the lines will be joined when rendered as Markdown. This is expected and correct.
A: Crashmail attempts direct delivery to the destination node instead of routing through your uplink. Enable it in binkp.json:
"crashmail": {
"enabled": true,
"max_attempts": 3,
"retry_interval": 3600
}(see binkp.json.example for complete configuration reference)
When sending netmail, check the "Crash" option to attempt direct delivery.
A: Check:
- The destination address is valid
- Your uplink is configured correctly in
binkp.json - The outbound directory is writable
- Run
php cli/binkp_poll.php --domain=<domain>to poll your uplink - Check
data/logs/packets.loganddata/logs/binkp_poll.logfor errors
A: It depends on the failure type:
- Single message exception (e.g. database error, malformed message data): Only that message is skipped. Processing continues normally for all remaining messages in the packet.
- Undeliverable netmail (no matching local user found by address or name): The message is dropped with a detailed log entry (from/to/subject/date/MSGID) and processing continues. The original
.pktfile is also preserved todata/undeliverable/for manual inspection. - Echomail from an insecure session (security rejection): Processing stops immediately — the rest of the packet is abandoned and moved to the error directory.
In the first two cases the packet is still considered successfully processed even if individual messages were skipped.
A:
Use the --queued-only switch to binkp_poll.php. In this mode binkp_poll will only poll the uplink if there are packets in the queue.
Q: If an uplink is configured to use CRAM-MD5 (crypt), can a remote system still dial in using plaintext authentication?
A: Yes, by default it can.
When the server sees that CRAM-MD5 is available it sends a challenge, but it will still accept a plaintext password response unless security.allow_plaintext_fallback is set to false in binkp.json.
So the current behavior is:
cryptenabled andallow_plaintext_fallback = true(default): CRAM-MD5 is offered, but plaintext is still acceptedcryptenabled andallow_plaintext_fallback = false: if a challenge was sent, the remote must use CRAM-MD5
Note: the server currently sends a CRAM-MD5 challenge if any configured uplink has crypt enabled, not only the specific remote that connected.
A:
- Polling (
binkp_poll.php): Your system initiates a connection to your uplink to send/receive mail - Binkp server (
binkp_server.php): Listens for incoming connections from other systems (downlinks, direct connects)
A: BinkP session activity is shown from the binkp_session_log database table. If a daemon child process exits before it can update the row, the session can remain marked as active even though the operating system process is gone.
First, confirm the stale row and note its id:
SELECT id, remote_address, remote_ip, process_id, log_file, started_at,
NOW() - started_at AS age
FROM binkp_session_log
WHERE status = 'active'
ORDER BY started_at;After confirming that the recorded process is not running, clear only that specific session row by id:
UPDATE binkp_session_log
SET status = 'failed',
ended_at = CURRENT_TIMESTAMP,
error_message = 'Manually cleared stale active session; no matching OS process found'
WHERE status = 'active'
AND ended_at IS NULL
AND id = 12345;Replace 12345 with the stale session id returned by the query. This keeps the audit record while removing it from the active sessions list. Targeting the session id is safer than targeting process_id, because operating system PIDs can be reused.
A: Usernames and real names must be unique (case-insensitive). If "John Doe" exists, another user cannot register as "john doe" or "JOHN DOE". This prevents impersonation in FidoNet messages.
A: Gateway tokens provide temporary authentication for external services:
- For example, User visits
/bbslinkwhile logged in - System generates a one-time token (valid for 5 minutes)
- User is redirected to the external service with the token
- External service calls back to verify the token via API
- Token is marked as used (single-use)
A: Set the 'admin' flag when editing the user through the web interface
A: Update the user's is_admin flag in the database:
UPDATE users SET is_admin = TRUE WHERE username = 'username';A:
- System daemons and tools:
data/logs/ - PHP errors: Check your web server's error log
- Binkp sessions: The session logs are always recorded by the binkp daemons.
A: Check:
- The inbound directory path in
binkp.jsonis correct - The packet processor is running:
php cli/process_packets.php - Check
data/logs/packets.logfor parsing errors - Verify the echo area exists and is active
A: Ensure:
- Your server's timezone is set correctly
- PostgreSQL timezone matches your expectations
- The
timezonesetting inbinkp.jsonis correct - PHP's
date.timezoneis configured inphp.ini
A:
- Enable verbose logging in your uplink's configuration
- Check
data/logs/binkp_poll.logafter a poll attempt anddata/logs/binkp_server.logfor incoming connections - Try connecting manually:
telnet hub.example.com 24554 - Verify your password matches what your uplink expects
- Check firewall rules for accept policies for outbound and inbound port 24554
A: Use the echomail maintenance script:
# Preview what would be deleted (dry run)
php scripts/echomail_maintenance.php --echo=all --domain=fidonet --max-age=365 --dry-run
# Actually delete old messages
php scripts/echomail_maintenance.php --echo=all --domain=fidonet --max-age=365A: Purge the echo's messages first, then delete the area:
# Preview purge for a single echo (dry run)
php scripts/echomail_maintenance.php --echo=YOUR_ECHO_TAG --domain=fidonet --max-count=0 --dry-run
# Purge all messages for that echo
php scripts/echomail_maintenance.php --echo=YOUR_ECHO_TAG --domain=fidonet --max-count=0Then delete the echo area in the admin UI.
A:
php cli/import_nodelist.php --domain=fidonetThis reads the nodelist configuration from data/nodelists.json and imports entries.
A: Use file area rules to automatically process nodelist files. Edit config/filearea_rules.json and add a rule for your NODELIST file area:
{
"area_rules": {
"NODELIST@fidonet": [
{
"name": "Auto-import FidoNet Nodelist",
"pattern": "/^NODELIST\\.(Z|A|L|R|J)[0-9]{2}$/i",
"script": "php %basedir%/scripts/import_nodelist.php %filepath% %domain% --force",
"success_action": "delete",
"fail_action": "keep+notify",
"enabled": true,
"timeout": 300
}
]
}
}Key points:
- Use
TAG@DOMAINformat for the area key (e.g.,NODELIST@fidonet) - Pattern matches compressed nodelist formats (Z=arc, A=zip, L=lha, R=rar, J=7z)
%basedir%macro expands to your BinktermPHP root directory%filepath%is the full path to the received file%domain%is the file area's domain (e.g., "fidonet")--forceflag makes the import script overwrite existing nodelist datasuccess_action: "delete"removes the file after successful importfail_action: "keep+notify"keeps the file and notifies sysop on failuretimeout: 300allows up to 5 minutes for large nodelists
The rule runs automatically when a file matching the pattern is uploaded or received via TIC.
For more details on file area rules, see docs/FileAreas.md.
A: Edit data/nodelists.json to specify nodelist sources and import settings. See the README for detailed configuration options.
A: There are two places to update:
1. File area setting (Admin → Area Management → File Areas)
Edit the file area and increase the Max File Size field. This controls the per-upload limit enforced by BinktermPHP itself.
2. PHP upload limits (php.ini)
PHP imposes its own limits that must be at least as large as the value above. Edit your php.ini (typically /etc/php/8.x/fpm/php.ini or /etc/php/8.x/apache2/php.ini):
upload_max_filesize = 100M
post_max_size = 110Mupload_max_filesize— maximum size of a single uploaded filepost_max_size— maximum size of the entire POST request body; set this slightly larger thanupload_max_filesize
After editing php.ini, restart your PHP process:
# PHP-FPM
sudo systemctl restart php8.x-fpm
# Apache mod_php
sudo systemctl restart apache2You can verify the active values with:
php -r "echo ini_get('upload_max_filesize'), ' / ', ini_get('post_max_size'), PHP_EOL;"A: Yes. Configure multiple uplinks in binkp.json with different domain values:
"uplinks": [
{ "address": "1:234/567", "domain": "fidonet", ... },
{ "address": "21:1/100", "domain": "fsxnet", ... }
]Each echo area is associated with a domain, and messages are routed to the appropriate uplink.
A: Edit the echo and file area and change its domain. Note that existing messages will retain their original routing information.
A: LovlyNet is a FidoNet Technology Network (FTN) operating in Zone 227 that provides automated registration and configuration. You can join the network and get an FTN address assigned automatically without manual coordination with a hub sysop.
For complete details, see docs/LovlyNet.md
A: Run the setup script:
php scripts/lovlynet_setup.phpThe script will guide you through registration, automatically configure your uplink, and subscribe you to default echo areas. See docs/LovlyNet.md for detailed instructions.
A:
- Public nodes: Accept inbound connections from the hub. Requires publicly accessible hostname/IP and working
/api/verifyendpoint. Hub can deliver mail directly to you. - Passive nodes: Poll-only mode for systems behind NAT, firewalls, or with dynamic IPs. No inbound connections accepted. Must poll the hub regularly.
See the "Public vs Passive Nodes" section in docs/LovlyNet.md for more details.
A: Run:
php scripts/lovlynet_setup.php --updateThis allows you to change your hostname, switch between public/passive modes, or update other registration details while keeping your FTN address.
A: Public nodes must have a working /api/verify endpoint. Test it:
curl https://yourbbs.example.com/api/verifyIf this fails, check:
- Web server is accessible from the internet
- HTTPS certificate is valid
- Firewall allows HTTP/HTTPS traffic
- If unavailable publicly, register as a passive node instead
See the "Troubleshooting" section in docs/LovlyNet.md for more help.
A: For passive nodes, set up a cron job:
# Poll once per hour — replace 23 with any minute 1–59 of your choosing.
# Picking a non-zero minute spreads load across the hub instead of
# every node polling at the top of the hour simultaneously.
23 * * * * cd /path/to/binkterm-php && php scripts/binkp_poll.php >> data/logs/poll.log 2>&1Public nodes can also poll as a fallback, though the hub will deliver mail directly via inbound connections.
A: Not necessarily. PostgreSQL uses sequential scans when they are more efficient than index scans, which is often the case for small tables or queries where a large fraction of rows would be returned. Here are the common cases you may see in BinktermPHP:
Small tables (users_meta, user_settings, mrc_state) For tables with only a few hundred or thousand rows, PostgreSQL's query planner will almost always prefer a sequential scan — the overhead of walking an index is greater than simply reading the table directly. High seq scan counts here are expected and correct behaviour, not an indexing gap.
High-frequency daemon tables (mrc_outbound, mrc_state) The MRC daemon polls these tables continuously (multiple times per second). Because these tables are very small, every poll results in a sequential scan. The counts look alarming but reflect normal operation.
OR-condition queries (chat_messages, shared_messages)
Queries that filter on col_a = ? OR col_b = ? cannot use a single B-tree index to satisfy both conditions simultaneously. PostgreSQL must either do a seq scan or merge two separate index scans (bitmap OR). For moderate table sizes or if the planner estimates a large result set, it will choose a seq scan even when indexes exist on both columns individually. This is a structural query pattern — adding more indexes will not change the planner's decision.
When to investigate further A high seq scan ratio is worth investigating when:
- The table is large (tens of thousands of rows or more)
- The query is selecting a small, specific subset of rows (highly selective filter)
- You can see a long-running or slow query in the Query Performance tab that targets that table
pg_stat_user_tables.seq_tup_readis very large relative toidx_tup_fetch
In those cases, review the actual queries and consider adding a targeted index. For the tables listed above, no additional indexing is needed.
A: It was an April Fools Day prank that raised awareness about the concerns of having an AI post to echomail areas in an unattended, automated fashion. We have no plans to add such functionality. Also, BinktermPHP 2.0 does not exist — at the time of this writing, we were only at version 1.9! And neither does ftp.mustang.com — from like, 2003. 😏