βββββββ βββ ββββββββββββββββββββ ββββββββββ βββ ββββββ ββββ βββ
βββββββββββ βββββββββββββββββββββββββββββββ ββββββββββββββββ βββ
βββββββββββ βββββββββββ βββ βββ ββββββββββββββββββββββ βββ
βββββββββββ βββββββββββ βββ βββ ββββββββββββββββββββββββββ
βββ ββββββββββββββββββββ βββ βββββββββββ ββββββ ββββββ ββββββ
βββ βββ βββββββ ββββββββ βββ ββββββββββ ββββββ ββββββ βββββ
Quick Start Β· Features Β· ChanNet API Β· Optional Integrations Β· Configuration Β· Backup System Β· Deployment Β· Themes Β· Changelog
RustChan is a fully-featured imageboard engine compiled into a single Rust binary. Deploy it on a VPS, a Raspberry Pi, or a local machine β no containers, no runtime, no package manager required. All persistent data lives in a single directory alongside the binary, making migrations as simple as cp -r.
ffmpeg is supported as an optional enhancement for video transcoding and audio waveforms. Tor onion service hosting is built in via Arti β no system tor installation required. Both degrade gracefully when disabled.
|
|
|
|
|
|
RustChan includes a two-layer federation and gateway system that runs automatically on port 7070 alongside the main web server. No additional configuration is required to enable it β if you want to federate with another RustChan node or integrate with a RustWave client, just start talking to port 7070.
Text only. No images, no media, and no binary data cross the ChanNet interface by design. All payloads are ZIP archives containing structured text (JSON manifests + plain
.txtpost bodies). Full schema documentation is inchannet_api_reference.docx.
These endpoints let RustChan nodes sync content with each other. All responses are ZIP archives.
| Endpoint | Method | Description |
|---|---|---|
/chan/export |
GET |
Export all posts from this node as a ZIP snapshot |
/chan/import |
POST |
Import a ZIP snapshot from a remote node |
/chan/refresh |
POST |
Pull fresh content from a known remote and apply it locally |
/chan/poll |
GET |
Lightweight poll β returns only new content since a given timestamp |
Quick example β pull content from a remote node:
# Export your node's posts
curl http://localhost:7070/chan/export -o my-export.zip
# Import a ZIP from another node
curl -X POST http://localhost:7070/chan/import \
-H "Content-Type: application/zip" \
--data-binary @remote-export.zip
# Refresh from a peer (supply the peer's export URL as the body)
curl -X POST http://localhost:7070/chan/refresh \
-H "Content-Type: text/plain" \
-d "http://peer.example.com:7070/chan/export"
# Poll for posts newer than a Unix timestamp
curl "http://localhost:7070/chan/poll?since=1741900000" -o delta.zipThe /chan/command endpoint exposes a typed JSON command interface for the RustWave audio transport client. Send a JSON command, receive a ZIP back. reply_push is the only command that writes anything to the database.
# Full export via command interface
curl -X POST http://localhost:7070/chan/command \
-H "Content-Type: application/json" \
-d '{"command": "full_export"}' \
-o full.zip
# Export a single board
curl -X POST http://localhost:7070/chan/command \
-H "Content-Type: application/json" \
-d '{"command": "board_export", "board": "b"}' \
-o board-b.zip
# Export a single thread
curl -X POST http://localhost:7070/chan/command \
-H "Content-Type: application/json" \
-d '{"command": "thread_export", "board": "b", "thread_id": 42}' \
-o thread-42.zip
# Export the archive for a board
curl -X POST http://localhost:7070/chan/command \
-H "Content-Type: application/json" \
-d '{"command": "archive_export", "board": "b"}' \
-o archive.zip
# Force a refresh from a peer
curl -X POST http://localhost:7070/chan/command \
-H "Content-Type: application/json" \
-d '{"command": "force_refresh", "peer": "http://peer.example.com:7070"}' \
-o result.zip
# Push a reply (the only write command)
curl -X POST http://localhost:7070/chan/command \
-H "Content-Type: application/json" \
-d '{
"command": "reply_push",
"board": "b",
"thread_id": 42,
"body": "Hello from RustWave"
}' \
-o result.zipPort 7070 is for node-to-node communication and RustWave integration. If you are running a public-facing instance and do not need federation, block port 7070 externally:
sudo ufw deny 7070/tcpIf you do want to federate with other nodes, allow port 7070 selectively rather than opening it to the world.
RustChan is fully functional without either tool. When detected at startup, additional capabilities activate automatically.
When ffmpeg is available on PATH:
- MP4 β WebM transcoding (VP9 + Opus) for maximum browser compatibility
- AV1 WebM β VP9 re-encoding for browsers without AV1 support
- Audio waveform thumbnails via the
showwavespicfilter - Video thumbnail extraction from the first frame for catalog previews
Without ffmpeg, videos are served in their original format and audio posts use a generic icon. Set require_ffmpeg = true in settings.toml to enforce its presence at startup. The ffmpeg execution timeout is configurable via ffmpeg_timeout_secs (default: 120).
See SETUP.md β Installing ffmpeg for platform-specific instructions.
RustChan includes built-in Tor onion service support via Arti β no system tor installation required. Set enable_tor_support = true in settings.toml and restart. On first launch RustChan will:
- Download ~2 MB of Tor directory data and bootstrap to the network (~30 seconds)
- Generate a persistent Ed25519 keypair in
rustchan-data/arti_state/keys/ - Derive your permanent
.onionaddress from that keypair and start the hidden service - Begin accepting and proxying inbound onion connections to the local HTTP port
The .onion address appears on the home page and in the admin panel as soon as the service is ready. Subsequent starts are ready in ~5 seconds using the cached consensus in rustchan-data/arti_cache/.
Back up rustchan-data/arti_state/keys/ β this directory contains your service keypair. Losing it means a new .onion address on the next start. Delete it intentionally to rotate to a new address.
See SETUP.md β Tor for details on key management and migrating from a previous system tor installation.
# 1. Build
cargo build --release
# 2. Create an admin account
./rustchan-cli admin create-admin admin "YourStrongPassword!"
# 3. Create boards
./rustchan-cli admin create-board b "Random" "General discussion"
./rustchan-cli admin create-board tech "Technology" "Programming and hardware"
# 4. Start the server
./rustchan-cliOpen http://localhost:8080 β the admin panel is at /admin.
On first launch, rustchan-data/settings.toml is generated with a fresh cookie_secret and all settings documented inline. Edit and restart to apply changes.
All data lives in rustchan-data/ alongside the binary. Nothing is written elsewhere unless explicitly overridden via environment variables.
rustchan-cli β single self-contained binary
rustchan-data/
βββ settings.toml β instance configuration (auto-generated)
βββ chan.db β SQLite database (WAL mode)
βββ full-backups/ β full site backups
β βββ rustchan-backup-20260304_120000.zip
βββ board-backups/ β per-board backups
β βββ rustchan-board-tech-20260304_120000.zip
βββ boards/
βββ b/
β βββ <uuid>.<ext> β uploaded files
β βββ thumbs/
β βββ <uuid>_thumb.jpg β auto-generated thumbnails & waveforms
βββ tech/
βββ <uuid>.<ext>
βββ thumbs/
Auto-generated on first run. Edit and restart to apply.
# Site identity
forum_name = "RustChan"
site_subtitle = "A self-hosted imageboard"
# Default theme served to new visitors. Options: terminal, frutiger-aero,
# dorific-aero, fluorogrid, neoncubicle, chan-classic
default_theme = "terminal"
# TCP port (binds to 0.0.0.0:<port>).
port = 8080
# Upload size limits (MiB).
max_image_size_mb = 8
max_video_size_mb = 50
max_audio_size_mb = 150
# Auto-generated on first run. Do not change after first use β
# existing IP hashes and bans will become invalid.
cookie_secret = "<auto-generated 32-byte hex>"
# Built-in Tor onion service (via Arti β no system tor required).
# First run bootstraps in ~30 s; keypair in rustchan-data/arti_state/keys/.
enable_tor_support = false
# Hard-exit if ffmpeg is not found (default: warn only).
require_ffmpeg = false
# Maximum time (seconds) to allow a single ffmpeg job to run.
ffmpeg_timeout_secs = 120
# WAL checkpoint interval in seconds (0 = disabled).
wal_checkpoint_interval_secs = 3600
# Automatic VACUUM: compact the database this many hours after startup,
# then repeat on the same interval. Set to 0 to disable.
auto_vacuum_interval_hours = 24
# Expired poll vote cleanup interval (hours). Vote rows for expired polls
# are deleted; poll questions and options are preserved.
poll_cleanup_interval_hours = 72
# Show a red warning banner in the admin panel when the DB exceeds this size.
db_warn_threshold_mb = 2048
# Maximum number of pending background jobs. Excess jobs are dropped with
# a warning log rather than causing OOM under a post flood.
job_queue_capacity = 1000
# Maximum waveform/thumbnail cache size per board's thumbs/ directory (MiB).
# A background task evicts oldest files when the limit is exceeded.
waveform_cache_max_mb = 200
# Tokio blocking thread pool size. Defaults to logical_cpus Γ 4.
# Tune downward on memory-constrained hardware (e.g. Raspberry Pi).
# blocking_threads = 16
# Archive overflow threads globally before any hard-delete, even on boards
# where per-board archiving is disabled.
archive_before_prune = trueAll settings can be overridden via environment variables, which take precedence over settings.toml.
| Variable | Default | Description |
|---|---|---|
CHAN_FORUM_NAME |
RustChan |
Site display name |
CHAN_SITE_SUBTITLE |
(from settings.toml) | Home page subtitle |
CHAN_DEFAULT_THEME |
terminal |
Default theme for new visitors |
CHAN_PORT |
8080 |
TCP port |
CHAN_BIND |
0.0.0.0:8080 |
Full bind address (overrides CHAN_PORT) |
CHAN_DB |
rustchan-data/chan.db |
SQLite database path |
CHAN_UPLOADS |
rustchan-data/boards |
Uploads directory |
CHAN_COOKIE_SECRET |
(from settings.toml) | CSRF tokens and IP hashing key |
CHAN_MAX_IMAGE_MB |
8 |
Max image upload size (MiB) |
CHAN_MAX_VIDEO_MB |
50 |
Max video upload size (MiB) |
CHAN_MAX_AUDIO_MB |
150 |
Max audio upload size (MiB) |
CHAN_THUMB_SIZE |
250 |
Thumbnail max dimension (px) |
CHAN_BUMP_LIMIT |
500 |
Replies before a thread stops bumping |
CHAN_MAX_THREADS |
150 |
Max threads per board before pruning/archiving |
CHAN_RATE_POSTS |
10 |
Max POSTs per rate window per IP |
CHAN_RATE_WINDOW |
60 |
Rate-limit window (seconds) |
CHAN_SESSION_SECS |
28800 |
Admin session duration (default: 8 hours) |
CHAN_BEHIND_PROXY |
false |
Trust X-Forwarded-For behind a reverse proxy |
CHAN_HTTPS_COOKIES |
(same as CHAN_BEHIND_PROXY) |
Set Secure flag on session cookies |
CHAN_WAL_CHECKPOINT_SECS |
3600 |
WAL checkpoint interval; 0 to disable |
CHAN_AUTO_VACUUM_HOURS |
24 |
Scheduled VACUUM interval (hours); 0 to disable |
CHAN_POLL_CLEANUP_HOURS |
72 |
Expired poll vote cleanup interval (hours) |
CHAN_DB_WARN_MB |
2048 |
DB size warning threshold (MiB) |
CHAN_JOB_QUEUE_CAPACITY |
1000 |
Max pending background jobs |
CHAN_FFMPEG_TIMEOUT_SECS |
120 |
Max duration for a single ffmpeg job |
CHAN_WAVEFORM_CACHE_MB |
200 |
Max waveform thumbnail cache per board (MiB) |
CHAN_BLOCKING_THREADS |
cpus Γ 4 |
Tokio blocking thread pool size |
CHAN_ARCHIVE_BEFORE_PRUNE |
true |
Archive globally before any hard-delete |
RUST_LOG |
rustchan-cli=info |
Log verbosity |
The entire backup system is accessible from the admin panel β no shell access required. All backup operations stream from disk in 64 KiB chunks; peak RAM overhead is roughly 64 KiB regardless of instance size. Backups are written to disk as temp files with an atomic rename on success, so partial backups never appear in the saved list.
A full backup is a .zip containing a consistent SQLite snapshot (via VACUUM INTO) and all uploaded files.
| Action | Description |
|---|---|
| πΎ Save | Creates a backup and writes it to rustchan-data/full-backups/ |
| β¬ Download | Streams a saved backup to your browser |
| βΊ Restore (server) | Restores from a file already on the server |
| βΊ Restore (upload) | Restores from a .zip uploaded from your computer (max 512 MiB) |
| β Delete | Permanently removes the backup file |
Board backups are self-contained: a board.json manifest plus the board's upload directory. Other boards are never affected.
Restore behaviour:
- Board exists β content is wiped and replaced from the manifest
- Board doesn't exist β created from scratch
- All row IDs are remapped on import to prevent collisions
Restore uses SQLite's
sqlite3_backup_init()API internally β pages are copied directly into the live connection, so no file swapping, WAL deletion, or restart is needed.
# Admin accounts
./rustchan-cli admin create-admin <username> <password>
./rustchan-cli admin reset-password <username> <new-password>
./rustchan-cli admin list-admins
# Boards
./rustchan-cli admin create-board <short> <name> [description] [--nsfw]
./rustchan-cli admin delete-board <short>
./rustchan-cli admin list-boards
# Bans
./rustchan-cli admin ban <ip_hash> "<reason>" [duration_hours]
./rustchan-cli admin unban <ban_id>
./rustchan-cli admin list-bans<short> is the board slug used in URLs (e.g. tech β /tech/). Lowercase alphanumeric, 1β8 characters.
See SETUP.md for a complete production guide covering:
- System user creation and hardened directory layout
- systemd service with security directives
- nginx reverse proxy with TLS via Let's Encrypt
- ffmpeg and Tor installation on Linux, macOS, and Windows
- First-run configuration walkthrough
- Raspberry Pi SD card wear reduction and blocking thread tuning
- Security hardening checklist
# ARM64 (Raspberry Pi 4/5)
rustup target add aarch64-unknown-linux-gnu
cargo install cross
cross build --release --target aarch64-unknown-linux-gnu
# Windows x86-64
rustup target add x86_64-pc-windows-gnu
cargo build --release --target x86_64-pc-windows-gnuThe release profile enables strip = true, lto = "thin", and panic = "abort". Typical binary size: 12β18 MiB.
RustChan is intentionally minimal β no template engine, no ORM, no JavaScript framework. HTML is rendered with plain Rust format! strings. The result is a single binary that starts in under a second.
| Layer | Technology |
|---|---|
| Web framework | Axum 0.8 |
| Async runtime | Tokio 1.x (manually sized blocking pool) |
| Database | SQLite via rusqlite (bundled, 32 MiB page cache, BEGIN IMMEDIATE transactions) |
| Connection pool | r2d2 + r2d2_sqlite (configurable size, 5-second acquisition timeout; exhaustion β 503) |
| Image processing | image crate + kamadak-exif for JPEG orientation; BMP/TIFF/SVG supported |
| Video transcoding | ffmpeg (optional, configurable timeout); GIFβWebM converted inline |
| Audio waveforms | ffmpeg showwavespic filter (optional) |
| Thumbnails | WebP output via ffmpeg or image crate fallback; SVG placeholders for video/audio/SVG |
| Password hashing | argon2 crate (Argon2id) |
| Timing-safe comparison | subtle crate |
| Response compression | tower-http CompressionLayer (gzip, Brotli, zstd) |
| Request timeout | tower-http TimeoutLayer |
| Logging | tracing + tracing-subscriber + daily-rotating file appender; logs in rustchan-data/ |
| HTML rendering | Plain Rust format! strings |
| Configuration | settings.toml (atomic writes) + env var overrides via once_cell::Lazy |
| Federation | ChanNet API on port 7070 (ZIP-based, text-only) |
| Tor onion service | Arti in-process (onion-service-service feature); keypair in arti_state/keys/ |
src/
βββ main.rs β entry point (~50 lines): runtime construction, CLI parsing, dispatch
βββ config.rs β settings.toml + env var resolution (atomic writes)
βββ error.rs β error handling and ban page rendering
βββ models.rs β database row structs (ip_hash is Option<String>)
βββ middleware/mod.rs β rate limiting, CSRF, IP hashing, proxy trust, request timeout
βββ workers/mod.rs β background job queue, media transcoding, cache eviction
βββ server/
β βββ server.rs β HTTP router, background task spawns, graceful shutdown
β βββ console.rs β terminal stats, keyboard console, startup banner
β βββ cli.rs β Cli / Command / AdminAction clap types, run_admin()
βββ media/
β βββ mod.rs β MediaProcessor, ProcessedMedia; public API
β βββ ffmpeg.rs β FFmpeg detection, subprocess execution, all ffmpeg helpers
β βββ convert.rs β per-format conversion logic (ConversionAction, convert_file)
β βββ thumbnail.rs β WebP thumbnail generation, SVG placeholders
β βββ exif.rs β EXIF orientation read/apply
βββ handlers/
β βββ admin/
β β βββ mod.rs β shared session helpers, re-exports
β β βββ backup.rs β all backup and restore handlers (rusqlite backup API)
β β βββ auth.rs β login, logout, session management
β β βββ moderation.rs β bans, reports, appeals, word filters, mod log
β β βββ content.rs β post/thread actions, board management
β β βββ settings.rs β site settings, VACUUM, admin panel
β βββ board.rs β board index, catalog, archive, search, thread creation
β βββ mod.rs β streaming multipart, shared upload helpers
β βββ thread.rs β thread view, replies, polls, editing
βββ db/
β βββ mod.rs β connection pool (configurable size), schema init, shared helpers
β βββ boards.rs β site settings, board CRUD, stats
β βββ threads.rs β thread listing, creation, mutation, archiving, pruning
β βββ posts.rs β post CRUD, file deduplication, polls, job queue
β βββ admin.rs β sessions, bans, word filters, reports, mod log, appeals
βββ templates/
β βββ mod.rs β base layout, pagination, timestamp formatting, utilities
β βββ board.rs β home page, board index, catalog, search, archive
β βββ thread.rs β thread view, post rendering, polls, edit form
β βββ admin.rs β login page, admin panel, mod log, VACUUM results, IP history
β βββ forms.rs β new thread and reply forms
βββ utils/
βββ crypto.rs β Argon2id, CSRF, sessions, IP hashing, PoW verification, password validation
βββ files.rs β upload validation, thumbnails (delegates to media/), EXIF stripping
βββ sanitize.rs β HTML escaping, markup (greentext, spoilers, dice, embeds)
βββ tripcode.rs β SHA-256 tripcode generation
| Concern | Implementation |
|---|---|
| Passwords | Argon2id (t=2, m=65536, p=2) β memory-hard, GPU-resistant |
| Brute-force | Progressive lockout after 5 failed admin login attempts per IP |
| Sessions | HttpOnly, SameSite=Strict, Max-Age aligned to server config |
| CSRF | Double-submit cookie with constant-time token comparison (subtle::ct_eq) |
| Security headers | CSP (script-src 'self', no unsafe-inline), HSTS (1 year + subdomains), Permissions-Policy |
| Inline JavaScript | Fully eliminated β all JS in external files; CSP enforced with no unsafe-inline |
| IP privacy | Raw IPs never stored or logged β HMAC-keyed SHA-256 hash used everywhere |
| Rate limiting | Sliding-window per hashed IP: POST endpoints (10/min), page-load GETs (60/min); /api/ routes excluded |
| Proxy support | All handlers use proxy-aware IP extraction when CHAN_BEHIND_PROXY=true |
| File safety | Content-Type + magic byte validation; file extensions never trusted |
| EXIF stripping | All JPEG uploads re-encoded β GPS, device IDs, and all metadata discarded; EXIF orientation applied before strip |
| XSS | All user input HTML-escaped before rendering; markup applied post-escape |
| Zip-bomb protection | Backup restore capped at 1 GiB per entry, 50,000 entries max |
| Backup upload cap | Full and board restore endpoints reject uploads over 512 MiB |
| Redirect hardening | Backslash and percent-encoded variants blocked on return_to parameters |
| Path traversal | Backup filenames validated against [a-zA-Z0-9._-] before filesystem access |
| Body limits | Per-route limits on small endpoints (64 KiB) to prevent memory exhaustion |
| Connection pool | Configurable pool size; 5-second acquisition timeout; pool exhaustion returns 503 (not 500) |
| PoW CAPTCHA | SHA-256 hashcash (20-bit difficulty), verified server-side with 5-minute grace window; covers threads and replies |
| PoW nonce replay | Used nonces tracked in memory; stale entries auto-pruned after the validity window expires |
| Job queue | Capped at job_queue_capacity; excess jobs logged and dropped, never causing OOM |
| Streaming uploads | Multipart fields validated against size limits in flight; per-field text caps (~100 KB body, ~4 KB name/subject) prevent OOM from oversized forms |
| Request timeout | Middleware terminates slow or stalled client connections; guards against slowloris-style attacks |
| Gateway IP safety | ChanNet gateway posts carry no IP address; ip_hash is nullable throughout β NULL rendered as empty string, never causes a 500 |
| Atomic config writes | settings.toml written via temp-file-then-rename; config never partially written on crash |
>quoted text greentext line
>>123 reply link to post #123
>>>/board/ cross-board index link
>>>/board/123 cross-board thread link (with hover preview)
**text** bold
__text__ italic
[spoiler]text[/spoiler] hidden until clicked or hovered
[dice NdM] server-side dice roll (e.g. [dice 2d6] β π² 2d6 βΈ β β
= 11)
:fire: :think: :based: :kek: β¦ (25 emoji shortcodes)
Six built-in themes, selectable via the floating picker on every page. Persisted in localStorage with no flash on load. The site-wide default for new visitors is set via default_theme in settings.toml or from the admin panel.
| Theme | Description |
|---|---|
| Terminal (default) | Dark background, matrix-green monospace, glowing accents |
| Frutiger Aero | Frosted glass panels, pearl-blue gradients, rounded corners |
| DORFic Aero | Dark stone walls, torchlit amber/copper glass panels |
| FluoroGrid | Pale sage, muted teal grid lines, dusty lavender panels |
| NeonCubicle | Cool off-white, horizontal scanlines, steel-teal borders |
| ChanClassic | Light tan/beige background, maroon accents, blue post-number links β classic imageboard styling |
See CHANGELOG.md for the full version history.
Latest β v1.1.0-alpha.2:
Tor migrated to Arti (built-in, no system tor required) β Arti bootstraps in-process at startup, derives a .onion address from a persistent keypair in arti_state/keys/, and proxies onion connections to the local HTTP port; no subprocess, no torrc, no hostname file polling Β· Critical fix: ChanNet gateway posts have no IP β ip_hash changed to Option<String> throughout (no more 500s on pages with gateway posts) Β· Log files now written to rustchan-data/ (not the binary directory) Β· Log file names fixed (rustchan.2024-01-15.log format) Β· Logs changed from dense JSON to human-readable text Β· Per-field multipart size caps (~100 KB body, ~4 KB name/subject) eliminate OOM risk from oversized form submissions Β· Poll duration overflow hardened Β· Backup system rewrites: rusqlite::backup API replaces fragile SQL string, RAII temp-file cleanup, pool exhaustion β 503 Β· DB pool size configurable; r2d2::Error correctly maps to 503 Β· All write transactions upgraded from DEFERRED to BEGIN IMMEDIATE Β· Rotating log files prevent disk exhaustion Β· 304 response builders fixed Β· Atomic settings.toml writes Β· Request timeout middleware (slowloris protection) Β· Worker JoinHandles persisted; graceful shutdown via CancellationToken + bounded await Β· Job recovery on startup for interrupted jobs Β· ChanNet server graceful shutdown unified with main HTTP server Β· Background tasks use tokio::select! for clean cancellation
v1.1.0-alpha.1:
ChanNet API on port 7070 (federation + RustWave gateway) Β· Major codebase refactor: main.rs shrunk from 1,757 β ~50 lines; handlers/admin.rs split into 6 focused files; new server/ module (server, console, CLI); new src/media/ module (ffmpeg, convert, thumbnail, exif) Β· BMP, TIFF, SVG upload support Β· GIFβWebM inline conversion Β· All thumbnails output as WebP Β· SVG placeholders for video/audio/SVG sources Β· PNGβWebP with size-check fallback Β· Atomic temp-then-rename for all conversions
v1.0.13:
Scheduled VACUUM Β· expired poll vote cleanup Β· DB size warning banner Β· job queue back-pressure Β· duplicate media job coalescing Β· configurable ffmpeg timeout Β· global archive_before_prune flag Β· waveform cache eviction Β· streaming multipart Β· ETag / Conditional GET (304) Β· gzip/Brotli/zstd response compression Β· manual Tokio blocking pool sizing Β· EXIF orientation correction Β· streaming backup I/O (peak RAM ~64 KiB) Β· ChanClassic theme Β· default_theme + site_subtitle in settings.toml Β· default theme selector in admin panel Β· admin panel reorganised Β· prepared statement caching audit Β· RETURNING clause for inserts Β· 32 MiB SQLite page cache Β· two new DB indexes (idx_posts_thread_id, idx_posts_ip_hash)
v1.0.12: Database module split into 5 focused files Β· template module split into 5 focused files Β· PoW bypass on replies fixed (critical) Β· PoW nonce replay protection Β· inline JS fully eliminated (script-src 'self' CSP) Β· backup upload size cap (512 MiB) Β· post rate limiting simplified Β· /api/ routes excluded from GET rate limit Β· trailing slash 404s fixed
v1.0.11: Security headers (CSP, HSTS, Permissions-Policy) Β· proxy-aware IP extraction on all handlers Β· GET rate limiting (60 req/min) Β· zip-bomb protection on restore Β· IP hashing everywhere Β· admin brute-force lockout Β· constant-time CSRF comparison Β· poll input caps Β· session cookie Max-Age Β· connection pool timeout Β· per-route body limits Β· open redirect hardening Β· worker exponential backoff Β· file dedup race fix Β· per-post ban+delete Β· ban appeal system Β· PoW CAPTCHA Β· video embeds Β· cross-board hover previews Β· new-reply pill Β· live thread metadata Β· "(You)" tracking Β· spoiler text
v1.0.9: Per-board editing toggle Β· configurable edit window Β· per-board archive toggle Β· AV1βVP9 transcoding fix
v1.0.8: Thread archiving Β· mobile reply drawer Β· dice rolling Β· sage Β· post editing Β· draft autosave Β· WAL checkpointing Β· VACUUM button Β· IP history
v1.0.7: EXIF stripping Β· image+audio combo posts Β· audio waveform thumbnails
v1.0.6: Web-based backup management Β· board-level backup/restore Β· GitHub Actions CI
v1.0.5: MP4βWebM auto-transcoding Β· home page stats Β· macOS Tor detection fix
Built with π¦ Rust Β Β·Β Powered by SQLite Β Β·Β Optional: ffmpeg Β Β·Β Tor built-in via Arti
Drop it anywhere. It just runs.