atxbbs.com › manual

Manual

Get ATXbbs running on a fresh Ubuntu droplet in about an hour. v0.1, last updated 2026-04-19.

Note: ATXbbs source is not yet publicly released. This manual documents the deployment of the reference implementation (austinspring.com). Source release is planned for v0.5 once the API surface stabilizes. If you want to run your own now, email terry@atxbbs.com.

Prerequisites

Step 1 — System packages

apt update && apt install -y python3 python3-venv python3-pip nginx \
    sqlite3 certbot python3-certbot-nginx rclone git ufw

ufw allow OpenSSH && ufw allow "Nginx Full" && ufw enable

Step 2 — Application directory

mkdir -p /opt/springbbs
cd /opt/springbbs
python3 -m venv venv
venv/bin/pip install --upgrade pip
venv/bin/pip install flask gunicorn werkzeug markdown bleach

Step 3 — Drop in the application files

Copy these from a release tarball (or current austinspring.com source):

/opt/springbbs/
├ app.py                  # main Flask app (~1,500 lines)
├ templates/
│  ├ base.html            # outer chrome, nav, flash messages
│  ├ conference.html      # live conference view
│  ├ thread.html          # live thread + reply form
│  ├ new_thread.html      # start-a-topic form
│  ├ signup.html / login.html / welcome.html
│  ├ settings.html / forgot.html / reset.html
│  ├ search.html / recent.html / members.html
│  ├ profile.html / admin.html
│  └ conference_welcome.html  # host-editable welcome (planned)
├ static/
│  └ spring.css           # phosphor CRT theme
├ .env                    # secrets (see Step 4)
└ venv/

Step 4 — Configure environment

Create /opt/springbbs/.env (chmod 600):

SPRINGBBS_SECRET=<generate with: python3 -c "import secrets; print(secrets.token_urlsafe(48))">
SPRINGBBS_SECURE=1                  # set to 1 in production for SECURE cookies

# Resend HTTPS API for outbound mail (DigitalOcean blocks SMTP)
RESEND_API_KEY=re_xxxxxxxxxxxxx
RESEND_FROM='Sysop <terry@yourdomain.com>'
RESEND_REPLY_TO=terry@yourdomain.com

Step 5 — Initialize the database

cd /opt/springbbs
venv/bin/python3 -c "from app import init_db; init_db()"

# Create your sysop account (in-DB; can also be done via signup then promote):
sqlite3 spring.db "
  INSERT INTO users (username, password_hash, joined_at, last_seen, is_admin)
  VALUES ('terry', '<hash>', datetime('now'), datetime('now'), 1);
"
# Generate the password hash:
venv/bin/python3 -c "from werkzeug.security import generate_password_hash; print(generate_password_hash('your-password-here'))"

Step 6 — systemd unit for gunicorn

cat > /etc/systemd/system/springbbs.service << 'EOF'
[Unit]
Description=ATXbbs gunicorn
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/springbbs
EnvironmentFile=/opt/springbbs/.env
ExecStart=/opt/springbbs/venv/bin/gunicorn \
    --workers 1 --threads 4 --max-requests 200 \
    --bind 127.0.0.1:8920 \
    --access-logfile /var/log/springbbs-access.log \
    --error-logfile /var/log/springbbs-error.log \
    app:app
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload && systemctl enable --now springbbs

Step 7 — nginx vhost

cat > /etc/nginx/sites-available/yourdomain.com << 'EOF'
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/yourdomain.com;
    index index.html;

    location /bbs/live/ {
        proxy_pass http://127.0.0.1:8920/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location / {
        try_files $uri $uri/ $uri.html =404;
    }
}
EOF
ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Step 8 — SSL via Let's Encrypt

certbot --nginx -d yourdomain.com -d www.yourdomain.com --redirect

Step 9 — Backups

# Configure rclone for B2:
rclone config  # follow prompts; create remote named "b2"

# Set up nightly cron jobs:
cat > /etc/cron.d/atxbbs-backup << 'EOF'
15 3 * * * root rclone sync /var/www/yourdomain.com/ b2:yourbucket/yourdomain.com/
20 3 * * * root rclone copy /opt/springbbs/ b2:yourbucket/springbbs/ --exclude 'venv/**'
EOF

Step 10 (optional) — Reconstruct from Wayback

If you're reviving an old yapp BBS from Wayback Machine archives:

# 1. Discover all archived thread URLs
python3 wayback-gather.py --domain spring.net > cdx.json

# 2. Fetch each thread at its latest captured timestamp
python3 fetch-from-wayback.py --cdx cdx.json --out /opt/atxbbs-tools/archive/threads/

# 3. Synthesize conference indexes from cached threads
python3 synthesize-conf-indexes.py /opt/atxbbs-tools/archive/threads/

# 4. Render to static HTML
python3 build-thread-pages.py --all
python3 build-conference-pages.py
Note: the reconstruction toolkit is currently bundled with the reference implementation. Standalone release planned for v0.3.

Day-to-day operations

Restart the app

systemctl restart springbbs
journalctl -u springbbs -n 50

Add a new conference

# 1. Add to CONFERENCES dict in /opt/springbbs/app.py
# 2. Add to CONFERENCES dict in build-conference-pages.py
# 3. Create empty index in conferences-union/<slug>.html
# 4. Run build-conference-pages.py to render the static landing
# 5. Restart Flask

Promote a user to admin

sqlite3 /opt/springbbs/spring.db \
  "UPDATE users SET is_admin=1 WHERE username='handle';"

Soft-delete a spam comment

Sysop opens https://yourdomain.com/bbs/live/admin, finds the row, fills in a reason, clicks delete. Posts hide from public views; data preserved for undo. (Or via SQL: UPDATE archive_comments SET deleted_at=datetime('now'), deleted_reason='spam' WHERE id=?;)

Restore from B2

# Restore live HTML
rclone sync b2:yourbucket/yourdomain.com/ /var/www/yourdomain.com/

# Restore Flask app + DB
rclone copy b2:yourbucket/springbbs/ /opt/springbbs/

# Or restore from a dated snapshot
rclone lsd b2:yourbucket-snapshots/  # list available dates
rclone copy b2:yourbucket-snapshots/atxbbs-2026-04-15/ /tmp/restore/
Pre-launch hardening checklist:

Troubleshooting

SymptomLikely cause + fix
HTTP 502 on /bbs/live/*Flask down. systemctl status springbbs; check /var/log/springbbs-error.log for syntax errors after edits.
Welcome email not sendingRESEND_API_KEY not in env, or domain not verified at Resend, or User-Agent missing (Cloudflare blocks default Python UA — set explicit UA).
Conference shows fewer threads than expectedConference index file missing topic numbers. Re-synthesize: python3 synthesize-conf-indexes.py <conf> then build-thread-pages.py <conf>.
CSRF errors on form submitPage cached without fresh token. Hard refresh (Ctrl-F5) and resubmit.
Rate-limited 429 unexpectedlyRestart Flask to clear in-memory state, or adjust per-route limits in rate_limit() calls.