Status: Early-stage (pre-1.0). Interfaces may change. Feedback & contributions welcome.
RIB (Rust Image Board) is a high-performance, self-hostable image board backend built in Rust. It provides a RESTful API for managing posts, images, comments, and user interactions in a thread-based discussion format similar to traditional image boards.
- Fast & resource-light
- Horizontally scalable
- Pluggable storage/auth
- Memory-safe (Rust)
- Simple ops
- Real-time (post-v1)
Thread - top-level post
Reply - comment in a thread
Board - category of threads
Bump - reply that lifts a thread
Sage - non-bumping reply
Single Actix-web service backed by:
• pluggable RDBMS (PostgreSQL, SQLite)
• optional Redis cache
• S3-compatible object store for file attachments
Key layers: API → Service → Repository → Storage/Cache.
See src/models.rs for full Rust definitions:
Board, Thread, Reply, File, Report (+ enums).
Validation highlights:
• board.slug: ^[a-z0-9_]{1,12}$
• post.content: 1-2000 chars
• attachments ≤ 25 MB, supports images, videos, documents, archives, and other common file types
OpenAPI spec: /docs/api/openapi.yaml
Major groups: boards, threads, replies, files, moderation.
Supports idempotency (header Idempotency-Key) and versioned base path /api/v1.
The system supports uploading various file types via the /api/v1/images endpoint:
Supported File Types:
- Images: PNG, JPEG, GIF, WebP, BMP, TIFF, SVG
- Videos: MP4, WebM, AVI, MOV, WMV, FLV
- Audio: MP3, WAV, OGG, FLAC, AAC, M4A
- Documents: PDF, Word (DOC/DOCX), Excel (XLS/XLSX), PowerPoint (PPT/PPTX), RTF, OpenDocument formats
- Text/Code: Plain text, CSV, HTML, CSS, JavaScript, JSON, XML, YAML
- Archives: ZIP, RAR, 7-Zip, TAR, GZIP, BZIP2
- Generic binary files: application/octet-stream
Limits:
- Maximum file size: 25 MB
- Content-Type detection via magic bytes
- SHA256 deduplication prevents duplicate storage
Primary storage uses PostgreSQL with the following considerations:
- UUID primary keys for global uniqueness
- Composite indexes on (board_id, bump_time) for thread listing
- Full-text search indexes on content fields
- Partitioning by board for large deployments
- Redis for hot data:
- Board catalog (5-minute TTL)
- Thread previews (1-minute TTL)
- Rate limiting counters
- Session data
- Primary: S3-compatible object storage (AWS S3, MinIO, etc.)
- CDN: CloudFlare or similar for global distribution
- Thumbnails: Generated on upload for images, stored separately
- Deduplication: SHA256 hash checking before storage
- Database: Point-in-time recovery (WAL shipping to cold storage).
- Files: Cross-region replication in object storage.
- Config: Encrypted off-site backups of
.envand Kubernetes secrets. - Quarterly recovery drills verify backup integrity.
| Data | Retention | Action |
|---|---|---|
| Threads (active) | indefinite | None |
| Threads (archived) | 365 days default | Purge files, keep metadata |
| Reports | 180 days | Hard delete |
| Audit logs | 730 days | Glacier/offline archive |
| Hashed IPs | 90 days | Rotating re-hash + purge old salts |
- Upload received (streamed, max size enforced early).
- MIME sniff + magic number verify.
- Hash (SHA256) while streaming -> dedupe check.
- Temporary quarantine storage.
- Optional scanning (clamd / external API).
- For images: Resize + thumbnail (libvips recommended).
- Commit metadata row + move to final bucket path.
- Emit event (internal) for cache warm.
- Use
sqlx migratewith strictly forward-only migrations in main branch. - Emergency rollback: deploy prior binary + run compensating forward migration (no down scripts in prod).
- Preflight: CI runs
sqlx prepareto verify offline query metadata.
- Anonymous posting: Default, with optional tripcodes
- Moderator auth: JWT tokens with role-based permissions
- API keys: For automated tools and bots
- Per-IP write limits:
- 1 thread per 5 minutes
- 10 replies per minute
- 5 file uploads per hour
- Per-IP read cap (ingress/CDN): ~120 requests per minute (burst 240)
- Exponential backoff for repeated violations
- CAPTCHA: Required for thread creation
- Spam detection: Bayesian filter for common spam patterns
- File validation: File type verification, virus scanning
- XSS prevention: Sanitize all user input
- CSRF protection: Token validation for state-changing operations
- IP logging: Hashed IPs only, rotated monthly
- GDPR compliance: Data export and deletion APIs
- No tracking: No analytics or third-party scripts
- DMCA / EUCD: Takedown workflow via
/api/reports. - COPPA: 13+ age gate banner for US visitors.
- Accessibility: WCAG 2.1 AA targets for future frontend.
Pluggable hook that sends file hashes to third-party services (e.g., PhotoDNA) before the file is made public. Failing files are quarantined.
Returned default headers:
Content-Security-Policy: default-src 'none'; img-src 'self' data: https:;X-Content-Type-Options: nosniffReferrer-Policy: no-referrerPermissions-Policy: interest-cohort=()Strict-Transport-Security: max-age=63072000TLS: Recommend TLS 1.3 only (configurable fallback).
- Weekly
cargo auditin CI. - Monthly dependency freshness report.
- CVE triage SLA: Critical 24h, High 72h, Medium 14d.
This section was consolidated from the former
docs/design.md.
RIB assumes basic application‑layer limits; for internet exposure you should pair it with an edge / CDN / WAF service to shed volumetric or brute‑force traffic earlier.
Typical pattern (example: Azure Front Door + WAF, but Cloudflare / Fastly / AWS equivalents work):
- Global rate limit (broad per‑IP cap, e.g. 100 req/min) before origin
- Narrow auth brute force rule (e.g. 5 login attempts/min on
/api/auth/login) - (Optional) geo, bot, or reputation based rules
Minimal rule table:
| Rule | Threshold | Window | Scope | Purpose |
|---|---|---|---|---|
| GlobalRateLimit | 100 | 1 min | All paths | Baseline volumetric damping |
| AuthRateLimit | 5 | 1 min | /api/auth/login |
Credential stuffing mitigation |
Operational notes:
- Tune after observing real traffic (export WAF logs to metrics store).
- Keep application aware only of business action limits (threads/replies/images) to reduce complexity.
- Consider synthetic probes for health endpoints so WAF doesn’t learn poor baselines.
Enhancements (future): adaptive thresholds, WAF blocked‑request alerting, automated CAPTCHA challenge escalation after repeated violations.
Implements reversible soft deletion and irreversible hard deletion for Boards, Threads, and Replies.
Goals:
- Soft delete hides content (
deleted_attimestamp) but allows restoration. - Hard delete permanently removes (leveraging FK cascades).
- Non‑admins always receive 404 for soft‑deleted content (no enumeration leaks).
- Admins can opt‑in to view deleted entities with
?include_deleted=1.
Schema additions: nullable deleted_at TIMESTAMPTZ on boards, threads, replies plus partial indexes on active rows:
CREATE INDEX idx_boards_not_deleted ON boards(id) WHERE deleted_at IS NULL;
CREATE INDEX idx_threads_board_active ON threads(board_id, bump_time DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_replies_thread_active ON replies(thread_id, created_at ASC) WHERE deleted_at IS NULL;
API (admin only):
POST /api/v1/admin/boards/{id}/soft-delete
POST /api/v1/admin/boards/{id}/restore
DELETE /api/v1/admin/boards/{id}
POST /api/v1/admin/threads/{id}/soft-delete
POST /api/v1/admin/threads/{id}/restore
DELETE /api/v1/admin/threads/{id}
POST /api/v1/admin/replies/{id}/soft-delete
POST /api/v1/admin/replies/{id}/restore
DELETE /api/v1/admin/replies/{id}
Query parameter: include_deleted=1 (honored only for admins) on selected GETs:
GET /api/v1/boardsGET /api/v1/boards/{id}/threadsGET /api/v1/threads/{id}GET /api/v1/threads/{id}/replies
Visibility rules:
| Actor | Deleted entity (no include) | With include_deleted=1 |
|---|---|---|
| Non‑admin | 404 | 404 |
| Admin | 404 | 200 + deleted_at |
Repository patterns:
- Soft delete:
UPDATE <table> SET deleted_at = COALESCE(deleted_at, now()) WHERE id=$1. - Restore:
UPDATE <table> SET deleted_at = NULL WHERE id=$1. - Hard delete:
DELETE FROM <table> WHERE id=$1. - Listings filter on
deleted_at IS NULLunless admin + include flag.
Edge cases tested:
- Soft delete hides from non‑admin lists/detail.
- Admin include flag reveals with
deleted_atpopulated. - Restore returns entity to active listings.
- Soft‑deleted thread hides replies (thread 404 for non‑admin).
- Hard delete cascades replies.
- Idempotent soft delete / restore.
Future enhancements (not yet implemented): deletion reasons, deleted_by, moderation audit log, scheduled purge, bulk moderation actions.
Design rationale consolidated here; original standalone design document removed to prevent drift.
- Connection pooling with
deadpool-postgres - Prepared statements for common queries
- Read replicas for scaling reads
- Materialized views for catalog pages
- Async I/O throughout with Tokio
- Response compression (gzip/brotli)
- ETag headers for caching
- Lazy loading of images
- Horizontal scaling behind load balancer
- Auto-scaling based on CPU/memory metrics
- Geographic distribution with edge caching
| Metric | Target |
|---|---|
| P95 thread list latency | < 80 ms |
| P99 reply post latency | < 150 ms |
| Max sustained RPS (single node) | 800 (read-heavy mix) |
| Cache hit ratio (catalog) | > 85% |
| Load tests executed prior to tagged releases; thresholds enforced in CI performance stage. |
Current (MVP) implementation: in-memory sliding window (per-process) using a lock-free dashmap.
Scopes & defaults (override with env vars):
- Threads: 1 per 300s (
RL_THREAD_LIMIT=1,RL_THREAD_WINDOW=300) - Replies: 10 per 60s (
RL_REPLY_LIMIT=10,RL_REPLY_WINDOW=60) - Images: 5 per 3600s (
RL_IMAGE_LIMIT=5,RL_IMAGE_WINDOW=3600)
Activation: set RL_ENABLED=1 (or true). If disabled, limiter is bypassed.
Algorithm: sliding window of Instants pruned on each check (O(k) with k << n based on small window sizes). Chosen for simplicity and low per-request overhead. Metrics exposed:
rate_limit_allowed{action}rate_limit_denied{action}
Limitations:
- Not distributed: each pod enforces independently (acceptable while upstream gateway/WAF supplies coarse global caps).
The original design doc also outlined a Redis-backed token bucket for strict global coordination. It was deferred for simplicity but remains the upgrade path when:
-
~10 pods and per‑pod multiplication becomes abusable
- Need for precise global quotas / billing metrics
- Desire for shared ban lists or reusable buckets across actions
Sketch (atomic Lua script pattern):
KEY: rl:{scope}:{normalized_ip}
Fields: tokens (int), refreshed_at (unix seconds)
Inputs: capacity, refill_rate (tokens/sec)
Algorithm:
now = time()
elapsed = now - refreshed_at
tokens = min(capacity, tokens + elapsed * refill_rate)
if tokens >= 1 then tokens -= 1; allow else deny
Pros:
- Single global view; exact fairness
- Easy temporary bans (expire key or set 0 tokens)
Cons:
- Adds infrastructure (Redis) for something edge/WAF already mitigates at coarse granularity
- Higher latency (extra network hop) vs in‑process
Migration approach:
- Ship feature-flagged Redis limiter alongside in-memory (shadow mode metrics)
- Compare allowance/denial divergence
- Flip default when divergence < acceptable threshold
- Remove in-memory path or keep as fallback
Until then the in‑process sliding window plus edge layer is considered "good enough" for early adoption.
- Application metrics: Request rate, latency, error rate
- Business metrics: Posts per hour, active users
- Infrastructure metrics: CPU, memory, disk I/O
- Structured logging with
tracing - Log levels: ERROR, WARN, INFO, DEBUG
- Centralized aggregation with ELK or similar
- Distributed tracing with OpenTelemetry
- Request ID propagation
- Performance profiling endpoints
- PagerDuty integration for P90 latency > 1 s or error rate > 2 %.
- Post-mortems required within 48 h of SEV-1 incidents.
- HTTP:
http_requests_total{method,route,status} - Latency:
http_request_duration_seconds_bucket{route} - DB:
db_query_duration_seconds_bucket{query_type} - Cache:
cache_operations_total{op,result} - Domain:
threads_created_total,replies_created_total,reports_open_total
- RED: Rate, Errors, Duration per top 10 routes.
- USE: Utilization, Saturation, Errors per resource (CPU, worker queue, DB pool).
# Start dependencies
docker-compose up -d postgres redis minio
# Run migrations
sqlx migrate run
# Start dev server with hot reload
cargo watch -x run- Unit tests: Business logic validation
- Integration tests: API endpoint testing
- Load tests: Performance benchmarking with k6
- Security tests: OWASP ZAP scanning
- Lint with
clippyandrustfmt - Run test suite
- Security audit with
cargo-audit - Build Docker image
- Deploy to staging
- Run smoke tests
- Deploy to production (blue-green)
- Pre-commit:
cargo fmt --allandcargo clippy --all-targets -- -D warnings - Dev-container:
.devcontainerfolder for VS Code + Docker ensuring reproducible envs.
RUST_LOG=debug RIB_PROFILE=devenables verbose SQL + feature flags.RIB_FEATURES="unsafe-fast-hash"(if added) would aid rapid prototyping (never in prod).
cargo run --bin seed populates sample boards & threads (feature-gated).
Planned: OpenAPI spec -> typed client via oapi-codegen (future).
FROM rust:1.75 as builder
# Multi-stage build for minimal image size
FROM debian:bookworm-slim
# Runtime with only necessary dependencies- Deployment: 3+ replicas for HA
- Service: LoadBalancer or NodePort
- ConfigMap: Environment configuration
- Secret: Database credentials, API keys
- HPA: Auto-scaling based on metrics
DATABASE_URL=postgres://user:pass@host/db
REDIS_URL=redis://host:6379
S3_BUCKET=rib-images
S3_ENDPOINT=https://s3.amazonaws.com
JWT_SECRET=<secure-random>
RUST_LOG=infoPriority order:
- Environment variables
config/{env}.toml- CLI flags (override all) Validation at startup; process aborts on missing required keys.
- Use readiness probe (healthz + version).
- Stagger termination grace period > max request time (e.g., 30s).
- Migrations run before pod replacement for additive changes.
- Basic CRUD for threads and replies
- In-memory storage for development
- Simple image upload
- Basic catalog view
- PostgreSQL integration
- Redis caching
- S3 image storage
- Rate limiting
- CAPTCHA integration
- Full moderation tools
- Search functionality
- WebSocket for live updates
- Archive system
- API versioning
- Microservices architecture
- GraphQL API option
- Federation support
- Machine learning for spam detection
| Risk | Mitigation |
|---|---|
| Image hash collision | SHA256 adequate; also check size/dimensions |
| Cache stampede on hot thread | Use request coalescing mutex (Redis SETNX) |
| Moderator key leak | Short-lived JWT + rotation schedule |
| Slow external hash check | Async queue + temporary placeholder thumbnail |
- Follow Rust standard conventions
- Use
rustfmtfor formatting - Write descriptive commit messages
- Add tests for new features
- Fork the repository
- Create feature branch
- Write tests and documentation
- Submit PR with description
- Address review feedback
- Prefer
anyhow::Resultinternally; map to API errors at handler boundary. - Use
#[instrument(skip(body))]for handlers; avoid logging raw images.
MIT License - See LICENSE file for details
- Pluggable ML scoring for spam (ONNX runtime).
- Adaptive image quality tiering (WebP/AVIF) negotiated via
Acceptheader. - Federation via ActivityPub subset for cross-instance thread mirroring.
| Topic | Question |
|---|---|
| Search | Use Postgres FTS vs external (Meilisearch) for scale > 10M posts? |
| Archive | Cold storage compression strategy? |
| Abuse | Automatic rate adaptation under volumetric attack? |
| Internationalization | How to store per-thread locale metadata? |
| ID | Decision | Rationale | Status |
|---|---|---|---|
| D1 | PostgreSQL primary store | Reliability + indexing | Accepted |
| D2 | Actix-web framework | Performance + ecosystem | Accepted |
| D3 | Redis optional cache | Avoid hard dependency for MVP | Accepted |
| D4 | S3 for images | Scalability vs local FS | Accepted |
A single-page application (SPA) that consumes the /api/v1 REST endpoints provided by RIB. It lives in a sibling repository rib-web, but its requirements are documented here to keep the product design cohesive.
• Framework: SvelteKit (lightweight, great SSR support)
• Language: TypeScript
• Styling: Tailwind CSS + DaisyUI components
• State: Svelte stores (board/thread cache)
• HTTP: @tanstack/query for request caching & deduplication
• Auth: JWT (stored in HttpOnly cookie) - shares secret with API
• Build: Vite (bundles, hot reload), ESLint + Prettier
| Route | Description |
|---|---|
/ |
Global board list |
/{slug} |
Thread catalog for a board |
/thread/{id} |
Thread view with infinite scroll replies |
/thread/new |
Create thread (CAPTCHA, image upload) |
/reply/{threadId} |
Quick-reply modal |
/mod |
Moderator dashboard (role-gated) |
ImageUpload- drag-and-drop, progress bar, MIME/size pre-checkPostForm- markdown editor with live previewThreadCard- board catalog entryReply- collapsible with quote-link parsingAuthGuard- protects moderator routes
All requests hit the same origin (served under /frontend) or a separate domain with CORS configured. fetch wrapper injects JWT, handles refresh, and maps HTTP errors to toast notifications.
rib-web is built into static assets (dist/) and either:
- Served by Nginx side-car container, or
- Embedded in the Actix binary behind
/when theembed-frontendfeature is enabled (usesinclude_bytes!).
- WCAG 2.1 AA compliance baseline
- Internationalization via
svelte-i18n, defaulten-US
Phase 1 (v0.1) - read-only pages
Phase 2 (v0.5) - posting flows + image upload
Phase 3 (v1.0) - moderation UI, live updates via WS
git clone https://github.com/johnkord/rib
cd rib
cp .env.example .env
openssl rand -base64 48 | tr -d '\n' | sed -e "s|CHANGE_ME_GENERATE_A_SECURE_SECRET|$(cat -)|" -i .env || true
docker compose up -d postgres redis minio
cargo run
# Open http://localhost:8080/docsOr fully containerized (backend too):
docker compose up -dThen visit:
- API: http://localhost:8080
- Docs: http://localhost:8080/docs
- MinIO Console: http://localhost:9001 (minioadmin / minioadmin)
| Task | Reason |
|---|---|
| Replace placeholder security/contact emails | Provide real disclosure channels |
Set strong JWT_SECRET (48+ bytes) |
Prevent JWT forgery |
| Harden Postgres creds & network policy | Reduce blast radius |
| Provision dedicated least-privilege S3 user | Principle of least privilege |
| Configure backups (DB WAL + object replication) | Disaster recovery |
Enable TLS + set ENABLE_HSTS=true |
Transport security |
| Add CI (fmt, clippy -D warnings, test, audit) | Supply chain & quality |
| External/global rate limiting (CDN/WAF) | Abuse mitigation |
| Review CSP before adding external scripts | Maintain XSS posture |
- Rust 1.75+
- Node.js 18+ (for frontend)
- Docker (optional, for dependencies)
- Clone the repository:
git clone https://github.com/yourusername/rib.git
cd rib-
Configure environment variables
Copy.env.exampleto.envand adjust values. -
Configure Discord OAuth (optional):
- Go to https://discord.com/developers/applications
- Create a new application
- Go to OAuth2 section
- Add redirect URI:
http://localhost:8080/api/v1/auth/discord/callback - Set
DISCORD_CLIENT_IDandDISCORD_CLIENT_SECRETenvironment variables
-
Run the server:
cargo run- (Optional) Develop frontend separately: The production build is now embedded in the Rust binary. For iterative frontend development you can still run Vite:
cd rib-react
npm install
npm run devWhen running the separate dev server, API calls target http://localhost:8080 and CORS is already configured.
To refresh the embedded assets used by the Rust binary during local (non-Docker) development:
cd rib-react
npm run build
cp -r dist ../embedded-frontend
cd ..
cargo runThe copy step updates the files that rust-embed packages at compile time; rebuild the Rust binary after changes.
See docs/dev-workflow.md for the evolving development environment design, parity strategy, and implementation checklist.
Common convenience targets (frontend container removed; backend serves SPA):
make dev-infra # start postgres, redis, minio
make dev-backend # run backend with auto-reload (needs cargo-watch)
make dev-frontend # run Vite dev server
make build-images # build all Docker images
make smoke # run quick curl-based smoke testsInstall local git hooks (pre-push smoke + tests) by pointing git to the provided hooks directory:
git config core.hooksPath .githooksDisable temporarily with:
git config --unset core.hooksPath| Variable | Required | Description |
|---|---|---|
JWT_SECRET |
Yes | Secret key for JWT signing (min 32 chars; recommend 48) |
DISCORD_CLIENT_ID |
No | Discord OAuth application ID |
DISCORD_CLIENT_SECRET |
No | Discord OAuth application secret |
DISCORD_REDIRECT_URI |
No | OAuth callback URL |
FRONTEND_URL |
No | Public origin of the SPA (default: http://localhost:8080 when embedded) |
BOOTSTRAP_ADMIN_DISCORD_IDS |
No | Comma‑separated Discord user IDs granted Admin on first login (e.g. 188880431955968000) |
ENABLE_HSTS |
No | Add HSTS header (only set true behind HTTPS) |
S3_ENDPOINT / S3_ACCESS_KEY / S3_SECRET_KEY / S3_BUCKET |
Yes (runtime) | Image storage (MinIO/S3) |
RL_ENABLED |
No | Enable in-memory rate limiting ("1"/"true") |
RL_THREAD_LIMIT / RL_THREAD_WINDOW |
No | Thread create limit & window seconds |
RL_REPLY_LIMIT / RL_REPLY_WINDOW |
No | Reply create limit & window seconds |
RL_IMAGE_LIMIT / RL_IMAGE_WINDOW |
No | Image upload limit & window seconds |
RUST_LOG |
No | Log level (info, debug, warn, error) |
When using VS Code's debugger, environment variables are automatically loaded from the .env file via the launch configuration in .vscode/launch.json.
To debug:
- Ensure
.envfile exists with your configuration - Press
F5or go to Run → Start Debugging - Select "Debug API (Rust)" or "Full-stack Dev"
Environment variables must be set before running the application:
# Linux/macOS - Set variables inline
JWT_SECRET="your-secret" RUST_LOG=info cargo run
# Or export them first
export JWT_SECRET="your-secret"
export RUST_LOG=info
cargo run
# Using a tool like direnv (install separately)
echo 'dotenv' > .envrc
direnv allow
cargo runIn production, set environment variables through your deployment platform:
- Docker: Use
--env-file .envor individual-eflags - Kubernetes: Use ConfigMaps and Secrets
- systemd: Use
Environment=directives in service files - Cloud platforms: Use their native environment configuration
Set BOOTSTRAP_ADMIN_DISCORD_IDS (comma separated) to automatically grant Admin role to those Discord IDs on first login without needing a prior role assignment.