- TypeScript 72.9%
- CSS 26.6%
- Dockerfile 0.4%
- HTML 0.1%
| scripts | ||
| src | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| bun.lock | ||
| Caddyfile | ||
| docker-compose.yml | ||
| Dockerfile | ||
| index.html | ||
| package.json | ||
| README.md | ||
| SECURITY.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
Discord Chat Archive
A self-hosted Bun and React website for importing or creating DiscordChatExporter archives, managing them under user accounts, and sharing them through controlled short links.
Included
- Open registration, email verification, password reset, secure database sessions, CSRF protection, and persistent rate limiting
- Optional administrator approval, account suspension, audit history, and bootstrap administrator promotion
- Existing HTML, JSON, TXT, CSV, and ZIP export uploads with bounded safe extraction
- Isolated DiscordChatExporter worker with saved or one-time RSA-encrypted credentials
- Optimized JSON exports split into 250-message chunks with a virtualized, Discord-style infinite-scroll viewer
- Private, unlisted, password-protected, expiring, public, and revocable short links
- Sandboxed legacy HTML viewing, archive ownership enforcement, and a public gallery
- PostgreSQL, Bun web/worker containers, Caddy, and optional Mailpit through Docker Compose
- Material 3 Expressive interface with a generated colour scheme, light and dark themes, and a responsive navigation rail
Quick start
Requirements: Bun, Docker, and Docker Compose.
cd /home/mopi/dev/discord-chat-archive
bun run setup
Edit .env before starting. At minimum, set BOOTSTRAP_ADMIN_EMAIL to the address that should become the first administrator. The setup command creates a random PostgreSQL password, an authenticated reverse-proxy secret, and a 4096-bit worker key pair without overwriting existing values or keys. Run it again after upgrading an existing checkout so newly introduced secrets are added safely.
For local development with a private email inbox:
docker compose --profile dev up --build -d
Open http://localhost:8080. Verification and reset messages appear in Mailpit at http://localhost:8025.
The first account registered with BOOTSTRAP_ADMIN_EMAIL becomes an administrator. If ADMIN_APPROVAL_REQUIRED=true, that account can approve later registrations under Administration.
Change the Docker Compose port
Set APP_PORT in .env, then recreate Caddy:
APP_PORT=9090
PUBLIC_ORIGIN=http://localhost:9090
docker compose up -d --force-recreate caddy web
APP_BIND controls the listening address. Use 127.0.0.1 when another reverse proxy is in front. HTTPS_PORT controls Caddy's HTTPS mapping.
Production deployment
- Set
PUBLIC_ORIGIN=https://archive.example.com. - Set
SITE_ADDRESS=archive.example.com,APP_PORT=80, andHTTPS_PORT=443if this Caddy instance should obtain and terminate HTTPS directly. Allow inbound ports 80 and 443. - Replace the Mailpit SMTP values with a real SMTP server and start Compose without the
devprofile. - Keep
.envandsecrets/worker_private.pemreadable only by the operator. Never commit either file. - Back up the
postgres_data,archive_data, andworker_private_keynamed volumes. Test restoration separately. Losing the worker key makes every saved Discord token undecryptable. - Review SECURITY.md and the upstream exporter's token warning before allowing registrations.
Useful commands:
docker compose up --build -d
docker compose logs -f web worker
docker compose ps
docker compose run --rm migrate
Worker key pair
The web service encrypts saved Discord tokens with a public key; only the worker holds the matching private key. Compose keeps the two halves in separate volumes, worker_public_key and worker_private_key, so the private key is never mounted into the web container.
The keygen service prepares them before anything else starts:
- If both keys already exist in the volumes, it leaves them untouched.
- Otherwise it imports
secrets/worker_public.pemandsecrets/worker_private.pemwhen that directory is populated, so an existing checkout that ranbun run setupkeeps decrypting its stored tokens. - Otherwise it generates a fresh 4096-bit pair. This is the normal path for a remote deployment, where the untracked
secrets/directory does not exist.
Remote hosts therefore need no manual secret provisioning: push the repository, set the environment values, and deploy. To move an existing instance to another host, copy the worker_private_key volume, or place the key pair in secrets/ before the first deployment.
Configuration
The documented defaults are in .env.example.
| Setting | Purpose |
|---|---|
APP_PORT / HTTPS_PORT |
Host ports mapped to Caddy |
PUBLIC_ORIGIN |
Canonical URL used for origin checks, emails, and short links |
SITE_ADDRESS |
Caddy site address; use :80 locally or a domain for automatic HTTPS |
REGISTRATION_ENABLED |
Allow new accounts |
EMAIL_VERIFICATION_REQUIRED |
Require verified email before sign-in |
ADMIN_APPROVAL_REQUIRED |
Put new accounts into pending status |
BOOTSTRAP_ADMIN_EMAIL |
Promote the matching account to administrator |
MAX_UPLOAD_MB |
Maximum uploaded source file size |
MAX_EXTRACTED_MB / MAX_ARCHIVE_FILES |
ZIP extraction limits |
MAX_EXPORT_MB / MAX_ARCHIVE_FILES |
Hard worker output limits, enforced while exporting |
MAX_USER_STORAGE_MB |
Per-account total storage quota for ready archives |
MAX_TOTAL_STORAGE_MB |
Instance-wide total storage quota for ready archives |
MAX_ACTIVE_EXPORTS |
Instance-wide cap for queued/running exports and uploads |
EXPORT_TIMEOUT_MINUTES |
Hard limit for one exporter process |
EXPORT_STALL_MINUTES |
Retry an exporter after this long without output or file activity |
AUDIT_RETENTION_DAYS |
Retention period for security audit events |
PROXY_SHARED_SECRET |
Setup-generated authentication between Caddy and the web service |
SMTP_* |
Verification and reset email delivery |
When changing MAX_UPLOAD_MB, also keep CADDY_MAX_BODY slightly higher to account for multipart framing.
Development and verification
bun install
bun run typecheck
bun test
bun run build
docker compose config
The container images are pinned by digest for reproducible builds. Update the version and digest together after reviewing an upstream release.
The web development server is bun run dev; the Vite client is bun run dev:ui. A PostgreSQL instance and generated worker public key are still required for server routes.
New server-side Discord exports use the optimized viewer automatically. Previously exported HTML and uploaded files remain available through the original sandboxed viewer; re-export an older conversation to make it lazy-loading and virtualized.
Interface
The web interface follows Material 3 Expressive. The design system lives in three files:
| File | Contents |
|---|---|
src/client/tokens.css |
Generated colour roles for light, dark, and high-contrast schemes |
src/client/styles.css |
Shape, type, elevation, state, and spring-motion tokens, then the components and screens built on them |
src/client/m3.tsx |
Buttons, fields, chips, cards, dialogs, snackbars, and the progress and loading indicators |
tokens.css is generated by Material's own colour utilities, so tones and contrast ratios are the ones the specification defines rather than hand-picked approximations. To re-theme the whole interface, change SOURCE in scripts/theme.ts and run:
bun run theme
Motion uses Material's damped-spring tokens, sampled into CSS linear() easings; the spatial springs overshoot slightly and the effects springs do not. Everything collapses to instant transitions under prefers-reduced-motion: reduce.
Readers choose light, dark, or system from the top bar, and the choice is stored per browser. Below 900px the navigation rail is replaced by a bottom navigation bar and a floating action button.
Token warning
DiscordChatExporter can infer and use both bot and user tokens. Its maintainers explicitly warn that automating a user account is against Discord's Terms and may get the account banned. This application does not block user tokens, but it shows that warning in the export flow and protects either token type identically.