No description
  • TypeScript 72.9%
  • CSS 26.6%
  • Dockerfile 0.4%
  • HTML 0.1%
Find a file
2026-08-28 19:41:18 +02:00
scripts feat: m3e redesign 2026-08-27 09:48:02 +02:00
src fix: fixes™️ 2026-08-28 19:41:18 +02:00
tests fix: fixes™️ 2026-08-28 19:41:18 +02:00
.dockerignore Initial commit 2026-08-27 08:42:39 +02:00
.env.example fix: discord export slowing down for some reason 2026-08-27 14:18:57 +02:00
.gitignore Initial commit 2026-08-27 08:42:39 +02:00
bun.lock feat: markdown support 2026-08-28 00:17:32 +02:00
Caddyfile Initial commit 2026-08-27 08:42:39 +02:00
docker-compose.yml fix: discord export slowing down for some reason 2026-08-27 14:18:57 +02:00
Dockerfile Initial commit 2026-08-27 08:42:39 +02:00
index.html feat: m3e redesign 2026-08-27 09:48:02 +02:00
package.json feat: markdown support 2026-08-28 00:17:32 +02:00
README.md fix: discord export slowing down for some reason 2026-08-27 14:18:57 +02:00
SECURITY.md fix 2026-08-27 12:08:45 +02:00
tsconfig.json Initial commit 2026-08-27 08:42:39 +02:00
vite.config.ts Initial commit 2026-08-27 08:42:39 +02:00

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

  1. Set PUBLIC_ORIGIN=https://archive.example.com.
  2. Set SITE_ADDRESS=archive.example.com, APP_PORT=80, and HTTPS_PORT=443 if this Caddy instance should obtain and terminate HTTPS directly. Allow inbound ports 80 and 443.
  3. Replace the Mailpit SMTP values with a real SMTP server and start Compose without the dev profile.
  4. Keep .env and secrets/worker_private.pem readable only by the operator. Never commit either file.
  5. Back up the postgres_data, archive_data, and worker_private_key named volumes. Test restoration separately. Losing the worker key makes every saved Discord token undecryptable.
  6. 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.pem and secrets/worker_private.pem when that directory is populated, so an existing checkout that ran bun run setup keeps 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.