BinktermPHP Configuration Reference

June 18, 2026 · View on GitHub

This document covers all configuration files and environment variables for BinktermPHP.

Most settings can be changed through the Admin web interface without editing files directly. Direct file editing is only necessary for initial setup, advanced options not yet exposed in the UI, or scripted/automated deployments.

After changing any configuration file, restart BBS services:

bash scripts/restart_daemons.sh

Table of Contents


Configuration Files Overview

FilePurposeEdited via
.envDatabase, SMTP, daemon ports, feature flagsText editor (initial setup)
config/binkp.jsonSystem identity, uplinks, binkp daemon, security, crashmailAdmin UI → BinkP Config
Database networks tableFTN network metadata and network-level message policy flagsAdmin UI → Networks
config/bbs.jsonBBS features (credits, file areas, registration, etc.)Admin UI → BBS Settings
config/nodelists.jsonNodelist download sourcesAdmin UI → Nodelists
config/matterbridge.jsonMatterbridge API bridge settings for local chatAdmin UI → Chat Rooms
config/mrc.jsonMRC chat relay serverAdmin UI or text editor
config/webdoors.jsonWebDoor game configurationAdmin UI → WebDoors
config/dosdoors.jsonDOS door game configurationAdmin UI → DOS Doors
config/nativedoors.jsonNative door configurationAdmin UI → Native Doors
config/themes.jsonTheme/appearance settingsAdmin UI → Appearance
config/taglines.txtPool of taglines appended to messagesText editor
config/weather.jsonWeather report API settingsAdmin UI or text editor

Examples for most files are provided as config/*.example — copy and edit as needed.


.env — Environment Variables

The .env file is the primary low-level configuration file. Copy .env.example to .env and fill in values before running setup.

.env.example is the authoritative list of all supported environment variables, each with its default value and a brief comment. The sections below highlight the most important options, but consult .env.example directly for the complete set.

Database

DB_HOST=localhost
DB_PORT=5432
DB_NAME=binktest
DB_USER=binktest
DB_PASS=yourpassword
DB_DRIVER=pgsql
DB_SSL=false

# Optional SSL (uncomment if your PostgreSQL requires it)
#DB_SSL_CA=/path/to/ca-cert.pem
#DB_SSL_CERT=/path/to/client-cert.pem
#DB_SSL_KEY=/path/to/client-key.pem

Site URL

SITE_URL=https://yourbbs.example.com

Used when generating share links, password-reset emails, and any absolute URL the server builds. Must be the public-facing URL including scheme. When behind a reverse proxy, set this explicitly — the server cannot reliably detect HTTPS from $_SERVER.

BBS Directory Geocoding

# Enabled by default
# BBS_DIRECTORY_GEOCODING_ENABLED=true
# BBS_DIRECTORY_GEOCODER_URL=https://nominatim.openstreetmap.org/search
# BBS_DIRECTORY_GEOCODER_USER_AGENT=
# BBS_DIRECTORY_GEOCODER_EMAIL=

These settings control the best-effort geocoding used to place BBS Directory entries on the public map. Coordinates are looked up from the entry's location field when a directory entry is created, updated, or upserted. If geocoding fails, the entry still saves normally and simply will not appear on the map until coordinates are available.

SMTP Email

SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURITY=tls          # tls, ssl, or empty
SMTP_USER=your@email.com
SMTP_PASS=yourpassword
SMTP_FROM_EMAIL=noreply@yourbbs.example.com
SMTP_FROM_NAME=MyBBS
SMTP_ENABLED=true
#SMTP_NOVERIFYCERT=true    # Uncomment to skip TLS cert verification

Used for new-user welcome emails, password resets, and sysop notifications.

Admin Daemon

ADMIN_DAEMON_SECRET=change_me_to_a_random_string

# Unix socket (Linux — recommended for production)
ADMIN_DAEMON_SOCKET=unix:///path/to/binkterm/data/run/admin_daemon.sock
ADMIN_DAEMON_SOCKET_PERMS=0660

# TCP socket (Windows, or if Unix socket is not available)
# ADMIN_DAEMON_SOCKET=tcp://127.0.0.1:9065

The admin daemon provides an internal API between the web interface and system services. Keep the Unix socket behind appropriate file permissions; the TCP socket should be bound to 127.0.0.1 only.

ADMIN_DAEMON_SCHEDULE_ENABLED=true
ADMIN_DAEMON_SCHEDULE_INTERVAL=60    # seconds between scheduler ticks

Telnet Daemon

# TELNET_BIND_HOST=0.0.0.0
# TELNET_PORT=2323

# TLS is enabled by default on port 8023.  Set false to disable.
# TELNET_TLS=true
# TELNET_TLS_PORT=8023
# TELNET_TLS_CERT=/etc/ssl/certs/your-cert.pem
# TELNET_TLS_KEY=/etc/ssl/private/your-key.pem

# ZMODEM file transfers over the telnet BBS
# TERMINAL_FILE_TRANSFERS=true
# TELNET_SZ_BIN=/usr/bin/sz   # override path to sz binary (lrzsz)
# TELNET_RZ_BIN=/usr/bin/rz   # override path to rz binary (lrzsz)
# TELNET_ZMODEM_DEBUG=false   # log to data/logs/zmodem.log

SSH Daemon

# SSH_PORT=2022
# SSH_BIND_HOST=0.0.0.0

See docs/SSHServer.md for full SSH daemon setup including key generation.

Gemini Capsule Daemon

# GEMINI_BIND_HOST=0.0.0.0
# GEMINI_PORT=1965
# GEMINI_CERT_PATH=/etc/letsencrypt/live/yourdomain.com/fullchain.pem
# GEMINI_KEY_PATH=/etc/letsencrypt/live/yourdomain.com/privkey.pem

See docs/GeminiCapsule.md for Gemini capsule hosting setup.

MCP Server

# MCP_SERVER_URL=https://yourbbs.example.com:3740  # Required to enable AI settings tab and key management
# MCP_SERVER_PORT=3740                              # Port the MCP server listens on (MCP_PORT also accepted)
# MCP_BIND_HOST=127.0.0.1                          # Bind to localhost when using a reverse proxy
# MCP_TRUSTED_PROXIES=127.0.0.1,::1,::ffff:127.0.0.1  # Proxy IPs whose X-Forwarded-For header is trusted

MCP_SERVER_URL must be set for users to see the AI settings tab and manage their bearer keys. It is the public-facing base URL of the MCP server (no trailing slash), e.g. https://yourbbs.example.com:3740 for the default direct listener, or https://mcp.yourbbs.example.com when using a reverse proxy or dedicated subdomain. See docs/MCPServer.md for full setup instructions.

Web Terminal (WebDoor)

TERMINAL_ENABLED=false             # Set true to enable the Terminal WebDoor
TERMINAL_HOST=your.ssh.server.com
TERMINAL_PORT=22
TERMINAL_TYPE=ssh                  # ssh or telnet
TERMINAL_PROXY_HOST=your.proxy.server.com
TERMINAL_PROXY_PORT=443
TERMINAL_TITLE=Terminal Gateway    # Label shown in the WebDoor

Requires a WebSocket-to-SSH proxy such as Terminal Gateway. Users must be authenticated in the web interface to access the terminal.

DOS Doors

DOSDOOR_WS_PORT=6001
DOSDOOR_WS_BIND_HOST=127.0.0.1
# DOSDOOR_WS_URL=wss://bbs.example.com:6001   # explicit client URL
DOSDOOR_MAX_SESSIONS=10
DOSDOOR_DISCONNECT_TIMEOUT=0        # 0 = close door immediately on disconnect
DOSDOOR_CARRIER_LOSS_TIMEOUT=5000   # ms to wait before force-kill

# DOSBOX_EXECUTABLE=/usr/bin/dosbox-x
# DOOR_EMULATOR=dosbox              # dosbox or dosemu
# DOSDOOR_CONFIG=dosbox-bridge-production.conf
# DOSDOOR_DEBUG_KEEP_FILES=false

See docs/DOSBox_Headless_Mode.md and docs/DOSDoors.md.

PubTerm (Native Door)

# PUBTERM_HOST=127.0.0.1
# PUBTERM_PORT=2323
# PUBTERM_TELNET_BIN=/usr/bin/telnet
# PUBTERM_PLINK_BIN=C:\Program Files\PuTTY\plink.exe   # Windows

Appearance

# STYLESHEET=/css/dark.css    # default stylesheet for all users
# FAVICONSVG=/robot_favicon.svg
# FAVICONPNG=/robot_favicon.png
# FAVICONICO=/robot_favicon.ico

Miscellaneous

# Virus scanning (ClamAV) — auto-detected if not set
# CLAMDSCAN=/usr/bin/clamdscan

# Slow-request profiling
PERF_LOG_ENABLED=false
PERF_LOG_SLOW_MS=500

# Echomail sort field (received = date_received, written = date_written)
# ECHOMAIL_ORDER_DATE=received

# Stale unprocessed inbound files are moved to data/inbound/unprocessed/ by default
# Files are only moved/deleted after they have been untouched for 24 hours
# Set this to true to delete stale unprocessed files instead of quarantining them
# BINKP_DELETE_UNPROCESSED_FILES=false

# Archive extractors for Fidonet bundles (JSON array)
# ARCMAIL_EXTRACTORS=["7z x -y -o{dest} {archive}","unzip -o {archive} -d {dest}"]

# i18n missing-key logging (development/QA)
# I18N_LOG_MISSING_KEYS=false
# I18N_MISSING_KEYS_LOG_FILE=data/logs/i18n_missing_keys.log

# File area rule action log
FILEAREA_RULE_ACTION_LOG=data/logs/filearea_rules.log

# ⚠️  DEVELOPMENT MODE — NEVER enable on a production system.
# Activates destructive diagnostic functions that can disrupt normal operation,
# including the ability to purge per-user QWK state (conference pointers,
# download log, message index, and deduplication hashes) via the web UI.
# Any feature gated on IS_DEV is intentionally undocumented elsewhere because
# it must not be accessible on a live system.
# IS_DEV=true

config/binkp.json — Core BBS Settings

config/binkp.json defines your system's FTN identity, the binkp daemon, uplinks, and related services. See config/binkp.json.example for an annotated reference.

Network-level message policy settings are not stored in config/binkp.json. Manage each network's display name, outgoing default charset, missing-CHRS fallback charset, markup setting, inline media setting, and posting-name policy from Admin → Networks. Uplink entries keep only connection, routing, polling, and authentication details and reference a network through their domain value.

After editing this file, restart services.

Minimal example:

{
    "system": {
        "name": "My BBS",
        "address": "1:123/456.0",
        "sysop": "Sysop Name",
        "location": "Anytown, USA",
        "hostname": "bbs.example.com",
        "timezone": "America/New_York"
    },
    "binkp": {
        "port": 24554,
        "timeout": 300,
        "max_connections": 10,
        "bind_address": "0.0.0.0",
        "inbound_path": "data/inbound",
        "outbound_path": "data/outbound",
        "outbound_queue_timer_minutes": 30,
        "preserve_processed_packets": false,
        "preserve_sent_packets": false
    },
    "uplinks": [ ... ],
    "security": { ... },
    "crashmail": { ... }
}

System Settings

FieldRequiredDescription
nameYesYour system's display name
addressYesYour primary FTN address (zone:net/node.point)
sysopYesSysop's real name — must match the real name on the sysop user account so netmail addressed to "sysop" delivers correctly
locationNoGeographic location shown in system info
hostnameYesYour internet hostname or IP address
websiteNoWebsite URL — included in FidoNet message origin lines when set
timezoneYesSystem timezone (PHP timezone list)

When website is configured, origin lines include it:

* Origin: My BBS <https://mybbs.example.com> (1:234/567)

Binkp Settings

FieldDefaultDescription
port24554TCP port for the binkp daemon
timeout300Connection timeout in seconds
max_connections10Maximum simultaneous inbound connections
bind_address0.0.0.0IP address to bind to (0.0.0.0 = all interfaces)
inbound_pathdata/inboundDirectory for incoming packets
outbound_pathdata/outboundDirectory for outgoing packets
outbound_queue_timer_minutes30After an immediate outbound delivery attempt, wait this many minutes before retrying while the queue remains non-empty
preserve_processed_packetsfalseWhen true, moves processed packets to data/inbound/keep/<Mon-DD-YYYY>/ instead of deleting them
preserve_sent_packetsfalseWhen true, moves successfully sent outbound packets to data/outbound/keep/<Mon-DD-YYYY>/ instead of deleting them

Each entry in the uplinks array defines one hub/uplink connection.

FieldRequiredDescription
meYesYour FTN address as presented to this uplink
addressYesThe uplink's FTN address
hostnameYesUplink hostname or IP address
portYesUplink port (typically 24554)
passwordYesAuthentication password (shared secret)
pkt_passwordNoPacket-level password (if different from session password)
tic_passwordNoTIC file password for file echoes
domainYesNetwork domain (e.g., "fidonet", "fsxnet", "agoranet")
networksYesAddress patterns routed through this uplink (see below)
poll_scheduleNoCron expression for automated polling, e.g. "0 */4 * * *"
send_domain_in_addrNoInclude @domain suffix in the ADR address sent to this uplink
enabledNoWhether uplink is active (default: true)
defaultNoWhether this is the default uplink for unrouted messages
compressionNoEnable compression (not yet implemented)
cryptNoEnable encryption (not yet implemented)
binkp_zoneNoDNS zone for crashmail fallback routing (e.g. "binkp.net")

Network Patternsnetworks uses wildcard patterns:

"1:*/*"   → all Zone 1 (FidoNet)
"21:*/*"  → all Zone 21 (FSXNet)
"46:*/*"  → all Zone 46 (AgoraNet)

Network domaindomain must match a configured network in Admin → Networks. Multiple uplinks may use the same domain, for example a primary and backup FidoNet hub. They share the network row's markup, inline media, outgoing default charset, missing-CHRS fallback charset, and posting-name policy settings.

Multiple networks example:

"uplinks": [
    {
        "me": "1:123/456.0",
        "address": "1:1/23",
        "domain": "fidonet",
        "networks": ["1:*/*", "2:*/*", "3:*/*", "4:*/*"],
        "hostname": "hub.fidonet.example.com",
        "port": 24554,
        "password": "fido_secret",
        "poll_schedule": "*/15 * * * *",
        "default": true,
        "enabled": true
    },
    {
        "me": "21:1/999",
        "address": "21:1/100",
        "domain": "fsxnet",
        "networks": ["21:*/*"],
        "hostname": "hub.fsxnet.example.com",
        "port": 24554,
        "password": "fsx_secret",
        "poll_schedule": "*/30 * * * *",
        "enabled": true
    }
]

Security Settings

The security section controls insecure (passwordless) inbound binkp sessions.

FieldDefaultDescription
allow_insecure_inboundfalseAllow incoming connections without password authentication
insecure_inbound_receive_onlytrueInsecure sessions can only deliver mail, not pick up
require_allowlist_for_insecurefalseOnly allow insecure sessions from nodes in the Admin allowlist
max_insecure_sessions_per_hour10Rate limit for insecure sessions per remote address
allow_plaintext_fallbacktrueAllow plaintext fallback when CRAM-MD5 is available

Insecure sessions are typically used for receiving mail from nodes that don't have your password configured. The allowlist (Admin → Insecure Nodes) gives fine-grained control.

Crashmail Settings

The crashmail section controls immediate direct delivery of netmail, bypassing normal hub routing.

FieldDefaultDescription
enabledfalseEnable crashmail direct delivery
max_attempts3Maximum delivery attempts before marking failed
retry_interval_minutes15Minutes between retry attempts
use_nodelist_for_routingtrueLook up destination in nodelist for hostname/port
fallback_port24554Default port if not found in nodelist
allow_insecure_crash_deliveryfalseAllow crashmail delivery without password

DNS Fallback (binkp_zone): When a destination node cannot be found in the nodelist, crashmail can fall back to DNS. Set binkp_zone on the matching uplink to enable it:

{
    "me": "1:123/456.0",
    "binkp_zone": "binkp.net"
}

The hostname is derived from the FTN address using the standard convention:

f{node}.n{net}.z{zone}.{binkp_zone}

Examples:
  1:123/456  →  f456.n123.z1.binkp.net
  2:250/10   →  f10.n250.z2.binkp.net

Compatible with DNS-based registries such as binkp.net.


config/nodelists.json — Nodelist Sources

Defines sources for automatic nodelist downloads. See config/nodelists.json.example for reference.

{
    "sources": [
        {
            "name": "FidoNet",
            "domain": "fidonet",
            "url": "https://example.com/NODELIST.Z|DAY|",
            "enabled": true
        },
        {
            "name": "FSXNet",
            "domain": "fsxnet",
            "url": "https://bbs.nz/fsxnet/FSXNET.ZIP",
            "enabled": true
        }
    ]
}
FieldRequiredDescription
nameYesDisplay name for this nodelist source
domainYesNetwork domain identifier (must match an uplink domain)
urlYesDownload URL; supports date macros (see below)
enabledNoWhether this source is active (default: true)

URL Date Macros:

MacroDescriptionExample
|DAY|Day of year (1–366)23
|YEAR|4-digit year2026
|YY|2-digit year26
|MONTH|2-digit month01
|DATE|2-digit day of month22

config/bbs.json — BBS Feature Settings

config/bbs.json controls BBS-specific features: credits system, file areas, user registration, telnet/SSH settings, and more. The recommended way to edit this is through Admin → BBS Settings in the web interface.

A documented example is provided in config/bbs.json.example.


Other Config Files

FilePurposeReference
config/mrc.jsonMRC multi-relay chat server connectiondocs/MRC_Chat.md
config/webdoors.jsonWebDoor game settings and enable/disabledocs/WebDoors.md
config/dosdoors.jsonDOS door game drop files and node settingsdocs/DOSDoors.md
config/nativedoors.jsonNative Linux/Windows door programsdocs/NativeDoors.md
config/themes.jsonAppearance system shell assignmentsdocs/CUSTOMIZING.md
config/weather.jsonWeather report API key and defaultsdocs/Weather.md
config/lovlynet.jsonLovlyNet network registrationdocs/LovlyNet.md
config/taglines.txtOne tagline per line; randomly appended to messages
config/filearea_rules.jsonAutomated file area processing rulesdocs/FileAreas.md

Network Ports Reference

ServiceDefault PortProtocolDirectionConfigured In
Web interface (via Apache/Caddy/Nginx)80, 443HTTP/HTTPSInboundWeb server / reverse proxy
BinkP daemon24554TCPIn + Outconfig/binkp.jsonbinkp.port
Telnet daemon (plain)2323TCPInbound.env TELNET_PORT
Telnet daemon (TLS)8023TCP/TLSInbound.env TELNET_TLS_PORT
SSH daemon2022SSH-2/TCPInbound.env SSH_PORT
Gemini capsule daemon1965Gemini/TLSInbound.env GEMINI_PORT
Realtime WebSocket daemon6010WebSocket/TCPInbound or proxied.env BINKSTREAM_WS_PORT
DOS door WebSocket bridge6001WebSocketInbound.env DOSDOOR_WS_PORT
DOSBox bridge session range5000–5100TCPInternalBetween bridge and emulator
Admin daemon (TCP fallback)9065TCPlocalhost.env ADMIN_DAEMON_SOCKET
PostgreSQL5432TCPInternal.env DB_PORT
MRC relay (remote)5000 / 5001TCP / TLSOutboundconfig/mrc.json

Tips:

  • Expose only the services you actually run.
  • Bind internal services (admin daemon, DOSBox bridge, PostgreSQL) to 127.0.0.1.
  • Publish user-facing services through a reverse proxy (Caddy, Nginx, Apache) with TLS.

Apache: enabling gzip compression for JSON

Apache's mod_deflate does not compress application/json by default, so API responses (including the i18n catalog) are sent uncompressed. Add the following to your VirtualHost or .htaccess:

<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE application/json
    AddOutputFilterByType DEFLATE text/html text/css application/javascript text/plain image/svg+xml
    AddOutputFilterByType DEFLATE font/ttf font/opentype application/x-font-ttf application/x-font-otf
    # Note: woff and woff2 are already compressed internally — do not add them here
</IfModule>

Enable the module if not already active:

a2enmod deflate
systemctl reload apache2

Caddy: enabling compression for JSON and HTML

Caddy does not enable response compression unless you add an encode directive. To compress normal pages and API responses, while avoiding compression on the SSE stream endpoint, add:

@compressible {
    not path /api/stream
}
encode @compressible zstd gzip

Place this near the top of your site block, after root.

This will compress endpoints such as:

  • /api/messages/echomail
  • /api/messages/netmail
  • /api/i18n/catalog
  • regular HTML, CSS, and JavaScript responses

Avoid compressing /api/stream, because SSE can be negatively affected by buffering or delayed flush behavior when compression is in play.


Server Sizing & Tuning

This section helps you right-size your server and tune Apache, PHP-FPM, and PostgreSQL for the number of concurrent users you expect.

How SSE Affects php-fpm Worker Count

BinktermPHP uses BinkStream for browser realtime delivery. In SSE mode, the SharedWorker calls /api/stream, which holds a php-fpm worker open for SSE_WINDOW_SECONDS, delivering events as they arrive. A keepalive comment is sent every 15 seconds to prevent proxy timeouts. When the window expires the connection is cleanly closed and the SharedWorker reconnects immediately. The practical effect is that every online user occupies one php-fpm worker continuously while SSE is the active transport. In WebSocket mode, the standalone PHP BinkStream daemon handles the realtime connection instead.

This makes pm.max_children the most important tuning knob on the system. If all workers are occupied by SSE connections, regular page loads and API calls will queue — or fail entirely.

Rule of thumb: pm.max_children ≥ (concurrent users × 1.1) + 5

The dominant cost is one worker per online user held for the SSE window. The 10% overhead covers concurrent HTTP requests (page loads, API calls) — at typical usage patterns only a small fraction of users are mid-request at any given instant.

The extra 0.5× headroom covers simultaneous HTTP requests (page loads, API calls) that arrive while SSE workers are held.


Real-World Baseline (3 concurrent users)

Measured RSS with 3 active users, all optional services running (Gemini, MRC, SSH, telnet, multiplexer):

ServiceProcessesRSS (MB)
Apache (apache2)13201.4
Caddy (reverse proxy)149.6
php-fpm5164.5
PostgreSQL12451.0
admin_daemon119.1
binkp_server130.0
binkp_scheduler119.9
mrc_daemon119.6
gemini_daemon113.6
telnetd115.0
ssh_daemon351.7
multiplexing-server163.4
Total1,099 MB

Key per-unit costs derived from the above:

  • php-fpm worker: ~33 MB each
  • Apache worker: ~15 MB each
  • PostgreSQL backend: ~12–15 MB per connection
  • System service baseline (daemons + proxy, no users): ~350–400 MB

Capacity Planning by User Count

These figures assume all optional services are running. Deduct ~130 MB if you omit MRC, Gemini, and the multiplexer.

Concurrent usersphp-fpm workersRAM (minimum)RAM (recommended)vCPU
1–5111 GB1.5 GB1
5–20272 GB3 GB2
20–50604 GB5 GB2–4
50–15017010 GB13 GB4–8
150–30033520 GB26 GB8–16

DOSBox-X doors add ~60–100 MB RAM per active session (see DOSDoors.md). At a typical 1% concurrency rate, the additional footprint is modest — roughly one instance per 100 online users — but plan for it if your BBS is games-heavy. A deployment with 100 concurrent users running a popular door at 5% concurrency would need an extra ~400 MB on top of the figures above.

"Concurrent users" means distinct logged-in users with the BBS open in their browser. Because SSE runs through a SharedWorker, all tabs belonging to the same user on the same browser share one EventSource connection — multiple tabs do not multiply worker usage.


php-fpm Tuning

Edit your php-fpm pool file (typically /etc/php/8.x/fpm/pool.d/www.conf):

; Use dynamic process management
pm = dynamic

; Maximum workers — size this first (see table above)
pm.max_children = 25

; Workers kept alive when idle
pm.min_spare_servers = 3
pm.max_spare_servers = 8

; Workers started on boot
pm.start_servers = 5

; Recycle workers to prevent slow memory growth
pm.max_requests = 500

After changing, reload php-fpm:

systemctl reload php8.x-fpm

BINKSTREAM_TRANSPORT_MODE — currently supported values are auto, sse, and ws.

  • auto (default): prefer the standalone WebSocket daemon when it appears to be running, otherwise use SSE. If Apache is detected and SSE_WINDOW_SECONDS is not explicitly set, BinktermPHP defaults the SSE window to 2 seconds as an interim mitigation for Apache + php-fpm buffering behavior.
  • sse: force the standard SSE behavior and default SSE_WINDOW_SECONDS to 60 unless explicitly overridden.
  • ws: force the standalone PHP WebSocket realtime daemon for inbound events and commands.
# .env
BINKSTREAM_TRANSPORT_MODE=auto

BINKSTREAM_WS_BIND_HOST, BINKSTREAM_WS_PORT, BINKSTREAM_WS_PUBLIC_URL, and BINKSTREAM_WS_PID_FILE — settings for the standalone realtime WebSocket daemon (scripts/realtime_server.php).

  • BINKSTREAM_WS_BIND_HOST: daemon bind host, typically 127.0.0.1 behind a reverse proxy
  • BINKSTREAM_WS_PORT: daemon listen port, default 6010
  • BINKSTREAM_WS_PUBLIC_URL: browser-facing WebSocket URL, typically a proxied path such as /ws
  • BINKSTREAM_WS_PID_FILE: optional PID file path used by auto mode as a server-side hint that the daemon is running; if omitted, BinktermPHP uses its built-in default PID path
# .env
BINKSTREAM_WS_BIND_HOST=127.0.0.1
BINKSTREAM_WS_PORT=6010
BINKSTREAM_WS_PUBLIC_URL=/ws
BINKSTREAM_WS_PID_FILE=data/run/realtime_server.pid

FTPD_ENABLED, FTPD_BIND_HOST, FTPD_PORT, FTPD_PUBLIC_HOST, FTPD_PASSIVE_PORT_START, and FTPD_PASSIVE_PORT_END — settings for the standalone FTP daemon (scripts/ftp_daemon.php).

  • FTPD_ENABLED: enable or disable the standalone passive FTP daemon
  • FTPD_BIND_HOST: FTP control socket bind host, typically 0.0.0.0 for inbound access or 127.0.0.1 when fronted by another layer
  • FTPD_PORT: FTP control port, default 2121
  • FTPD_PUBLIC_HOST: optional public IPv4 address or hostname advertised in PASV replies when the daemon sits behind NAT or binds to 0.0.0.0
  • FTPD_PASSIVE_PORT_START / FTPD_PASSIVE_PORT_END: passive-mode data port range opened by the daemon

The FTP virtual filesystem exposes:

  • /qwk/download/<BBSID>.QWK for downloading the authenticated user's QWK packet
  • /qwk/upload/*.REP or /qwk/upload/*.ZIP for uploading REP reply packets
  • /incoming/<AREA>/... for uploading files into writable file areas using the existing pending-approval workflow
  • /fileareas/... for browsing and downloading approved files from accessible file areas

Anonymous FTP login is also supported. Anonymous users are restricted to /fileareas/... and only see file areas marked is_public; they cannot access QWK endpoints or upload anything.

See FTPServer.md for daemon startup, systemd/cron examples, NAT setup, and rootless port-21 redirect rules.

# .env
FTPD_ENABLED=false
FTPD_BIND_HOST=0.0.0.0
FTPD_PORT=2121
FTPD_PASSIVE_PORT_START=2122
FTPD_PASSIVE_PORT_END=2149

SSE_WINDOW_SECONDS — how long each SSE connection is held open before the client is told to reconnect. A keepalive comment is sent every 15 seconds inside the window to prevent proxy timeouts. When SSE is the active transport, each active browser session occupies one php-fpm worker for the full duration, so scale pm.max_children accordingly.

Defaults:

  • 60 seconds normally
  • 2 seconds when BINKSTREAM_TRANSPORT_MODE=auto, Apache is detected, and SSE_WINDOW_SECONDS is not explicitly set

Testing has shown that some Apache + php-fpm (mod_proxy_fcgi) deployments buffer SSE responses instead of flushing events in real time. In those environments, sysops should lower SSE_WINDOW_SECONDS explicitly if the automatic 2-second default is still too sluggish.

# .env
BINKSTREAM_TRANSPORT_MODE=auto
SSE_WINDOW_SECONDS=60

Older REALTIME_* environment variable names are still accepted as compatibility aliases, and SSE_TRANSPORT_MODE is still accepted as the oldest transport-mode alias, but BINKSTREAM_* is the preferred prefix going forward.


Apache MPM Tuning

For Apache + php-fpm (via mod_proxy_fcgi), use mpm_event — it handles idle keepalive connections with threads rather than processes, which is far more efficient than mpm_prefork.

a2dismod mpm_prefork
a2enmod mpm_event proxy_fcgi
systemctl restart apache2
# /etc/apache2/mods-enabled/mpm_event.conf
<IfModule mpm_event_module>
    StartServers          2
    MinSpareThreads      10
    MaxSpareThreads      30
    ThreadsPerChild      25
    MaxRequestWorkers   150   # should be ≥ pm.max_children
    MaxConnectionsPerChild 1000
</IfModule>

MaxRequestWorkers controls how many simultaneous connections Apache will accept. It should be at least as large as pm.max_children so Apache never queues requests that php-fpm has capacity to handle.


PostgreSQL Tuning

PostgreSQL spawns one backend process per connection. Each php-fpm worker can hold an open connection, so max_connections must exceed pm.max_children.

# postgresql.conf

# Must exceed pm.max_children + room for admin/scripts
max_connections = 100        # adjust up for larger deployments

# 25% of total RAM is a standard starting point
shared_buffers = 512MB       # for a 2 GB server

# Effective cache size hint for the query planner (~50–75% of RAM)
effective_cache_size = 1GB

# Background writer — tune for write-heavy loads
wal_buffers = 16MB
checkpoint_completion_target = 0.9

After editing postgresql.conf, reload:

systemctl reload postgresql

PgBouncer is worth considering at 50+ concurrent users. It pools application connections so that 100 php-fpm workers can share 20 actual PostgreSQL backends, reducing per-connection memory significantly.


Quick Sizing Reference

KnobFormulaNotes
pm.max_childrenconcurrent users × 1.1 + 51 SSE worker per user (SharedWorker)
MaxRequestWorkerspm.max_childrenKeep in sync
max_connections (PG)pm.max_children + 10Add more for scripts
shared_buffers (PG)25% of total RAMStandard rule of thumb
BINKSTREAM_TRANSPORT_MODEautoauto prefers WS if the daemon appears to be running, otherwise uses SSE; Apache + SSE falls back to a 2 s window unless overridden
BINKSTREAM_WS_PORT6010Port used by the standalone PHP realtime WebSocket daemon
BINKSTREAM_WS_PUBLIC_URL/wsBrowser-facing WebSocket URL or path
BINKSTREAM_WS_PID_FILEdata/run/realtime_server.pidPID file hint used by auto transport selection
FTPD_PORT2121FTP control port exposed by the standalone FTP daemon
FTPD_PASSIVE_PORT_START2122First port in the FTP passive data range
FTPD_PASSIVE_PORT_END2149Last port in the FTP passive data range
SSE_WINDOW_SECONDS60Default SSE window unless Apache + auto applies the 2 s implicit fallback

Welcome & Text Files

The recommended way to manage welcome text is through Admin → Appearance, which provides a live editor with Markdown support and instant preview — no file editing or service restart required.

Direct file editing is still supported as a fallback (useful for scripted deployments or when the admin UI is not yet accessible):

FileWhen shown
config/terminal_welcome.txtTelnet/SSH BBS login screen (replaces default host:port message)
config/newuser_welcome.txtEmail sent to newly approved users
config/welcome.txtGeneral welcome message on the main page / login screen

Files are plain text; newlines are preserved as written. When both a file and an Appearance editor value exist, the Appearance editor value takes precedence.