Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Midden

Midden is a self-hostable file and paste sharing service written in Rust. It provides browser workflows for quick uploads and pastes, account-owned items, delete tokens for anonymous items, optional public browsing, reports, moderation tools, API tokens, and operational controls for storage, scanning, jobs, metrics, and delivery.

The project is still in development. Treat each deployment as self-managed software: read the example configuration, keep backups, run upgrades intentionally, and validate your instance before trusting it with important data.

Main Components

  • The midden binary serves the web app and exposes maintenance commands.
  • Axum handles HTTP routing, middleware, web UI pages, and JSON API endpoints.
  • SQLx supports SQLite and PostgreSQL.
  • object_store stores blobs on local disk or S3-compatible storage.
  • Runtime settings are loaded from TOML and environment variables, then can be overridden through persisted admin settings.
  • Background jobs expire old items, clean auth state, retry scanner decisions, process metadata and thumbnails, and verify storage drift.

Documentation Map

  • Use the getting started guide to run a local or Compose-based instance.
  • Use the operator guide to configure production-like deployments.
  • Use the user guide for browser workflows.
  • Use the API guide for token-authenticated clients.
  • Use the contributor guide for codebase structure and validation expectations.

Quick Start

This quick start runs Midden from source with the default SQLite database and local blob storage.

Prerequisites

  • Rust 1.95 or newer with Cargo. The checked-in toolchain file pins 1.95.0 for reproducible local and CI builds.
  • A checkout of the Midden repository.
  • A shell with network access for the first dependency build.

Run From Source

From the repository root:

cargo run -- config print-defaults > midden.toml
cargo run -- migrate
cargo run -- owner create --email owner@example.test --username owner --password change-me
cargo run -- serve

Open http://127.0.0.1:8080.

The default configuration listens on 127.0.0.1:8080, stores SQLite data in midden.db, and stores file blobs under data/blobs.

Upload A File

Open the home page and choose a file, or use the API:

curl -F file=@example.txt http://127.0.0.1:8080/api/v1/files

By default, uploads can be anonymous. Anonymous items receive a delete token in the result; keep that token if you want to delete or later claim the item.

Create A Paste

Open /p/new, or use the API:

curl -H 'content-type: application/json' \
  -d '{"content":"hello","expires":"7d","visibility":"unlisted"}' \
  http://127.0.0.1:8080/api/v1/pastes

Check Health

curl http://127.0.0.1:8080/healthz
curl http://127.0.0.1:8080/readyz

/healthz confirms the HTTP process is alive. /readyz checks database and storage reachability.

Installation

Midden is distributed as a Rust binary and a Docker image build target in this repository.

Build The Binary

cargo build --release --locked --bin midden

The binary will be available at:

target/release/midden

Configure

Create a starting configuration:

midden config print-defaults > midden.toml

Edit at least:

[server]
bind = "0.0.0.0:8080"
public_base_url = "https://files.example.test"

[security]
secure_cookies = true

For local-only development, the defaults are enough.

Run The Container Directly

The image defaults to listening on 0.0.0.0:8080. Its default SQLite database and local blob paths are under the /var/lib/midden working directory, so mount that directory for persistence:

docker build -t midden:local .
docker run --rm -p 8080:8080 \
  -v midden-data:/var/lib/midden \
  -e MIDDEN__SERVER__PUBLIC_BASE_URL=http://localhost:8080 \
  midden:local

The image health check requests /healthz. Use the Compose models for the PostgreSQL and S3-compatible layout.

Migrate

Run migrations before serving, or let midden serve apply them at startup:

midden --config midden.toml migrate

Serve

midden --config midden.toml serve

Useful Commands

midden --config midden.toml config check
midden --config midden.toml owner create --email owner@example.test --username owner
midden --config midden.toml owner reset-password --email owner@example.test --password new-password
midden --config midden.toml user set-role --email user@example.test --role moderator
midden --config midden.toml storage verify
midden --config midden.toml jobs run-once

config check constructs the application state and validates that database, storage, templates, SMTP configuration, and other startup surfaces can be initialized.

Docker Compose

The repository includes one common Compose model and two explicit storage/database overrides. Always combine the base file with exactly one override.

SQLite And Local Storage

docker compose -f docker-compose.yml -f docker-compose.sqlite.yml up --build

This starts only the Midden service. Data is persisted in the midden-data volume at /var/lib/midden.

Important environment variables from this file:

MIDDEN__SERVER__BIND=0.0.0.0:8080
MIDDEN__SERVER__PUBLIC_BASE_URL=http://localhost:8080
MIDDEN__DATABASE__URL=sqlite:///var/lib/midden/midden.db?mode=rwc
MIDDEN__STORAGE__BACKEND=local
MIDDEN__STORAGE__LOCAL__PATH=/var/lib/midden/blobs

PostgreSQL And MinIO

docker compose -f docker-compose.yml -f docker-compose.postgres-minio.yml up --build

This starts Midden, PostgreSQL 17, MinIO, and a one-shot MinIO bucket initializer. It is useful for testing the PostgreSQL and S3-compatible paths without external services.

Validate The Models

docker compose -f docker-compose.yml -f docker-compose.sqlite.yml config --quiet
docker compose -f docker-compose.yml -f docker-compose.postgres-minio.yml config --quiet

The base file contains only settings shared by both deployments. The override selects the database, storage backend, dependencies, and durable volumes, so enabling an unrelated profile cannot leave Midden connected to SQLite while idle PostgreSQL or MinIO containers run beside it.

Production Notes

  • Set MIDDEN_PUBLIC_BASE_URL to the public HTTPS origin.
  • Use durable volumes or managed services for the database and blob storage.
  • Set MIDDEN__SECURITY__SECURE_COOKIES=true behind HTTPS.
  • Set MIDDEN__SERVER__BEHIND_PROXY=true when a trusted reverse proxy supplies client IP headers.
  • Do not use the example MinIO credentials outside local testing.

First Owner Setup

Owner accounts can access the admin area and manage users, settings, moderation, and jobs. Create at least one owner before exposing an instance that requires account administration.

Create An Owner

midden --config midden.toml owner create \
  --email owner@example.test \
  --username owner \
  --password 'change-me'

If --password is omitted, the owner is created without a local password. That can be useful for OIDC-only deployments, but only if OIDC is already configured and usable.

Reset An Owner Password

midden --config midden.toml owner reset-password \
  --email owner@example.test \
  --password 'new-password'

Promote An Existing User

midden --config midden.toml user set-role \
  --email user@example.test \
  --role owner

Valid roles are user, moderator, admin, and owner.

Avoid Lockouts

Midden has a server-side admin settings guard that rejects configurations where local login is disabled and OIDC is not actually enabled. Still, operators should stage authentication changes carefully:

  • Keep one tested owner session open while changing auth settings.
  • Confirm the configured OIDC issuer, client ID, client secret, redirect URL, and feature flags.
  • Keep a recovery path using the CLI and direct configuration access.

Configuration

Midden loads configuration from TOML and environment variables.

Source Order

The effective precedence, from lowest to highest, is:

  1. Compiled defaults.
  2. An explicit --config PATH, which must exist, or the optional midden.toml when no path is provided.
  3. Persisted admin settings for runtime-adjustable sections.
  4. Explicit MIDDEN__ environment fields.

Environment precedence is field-level. For example, MIDDEN__SECURITY__SECURE_COOKIES=true locks only security.secure_cookies to the environment value. Other fields in the persisted security section, such as rate limits and content policy, continue to come from the admin settings row. Removing the environment variable makes the persisted value for that field effective again.

Use this command to inspect the compiled defaults:

midden config print-defaults

Use this command before deploys:

midden --config midden.toml config check

Primary Sections

  • [server]: bind address, public base URL, optional template/static directories, reverse proxy mode.
  • [database]: SQL connection URL and pool size.
  • [storage]: local or S3-compatible blob storage.
  • [features]: feature switches for files, pastes, accounts, API, reports, URL upload, previews, browse, auth modes, and paste editing.
  • [limits]: upload sizes, paste sizes, expiry defaults, expiry guardrails, anonymous quota, and role quotas.
  • [branding]: instance name, tagline, colors, custom CSS, footer links, notices, Open Graph behavior, and takedown text.
  • [policy]: signup mode and action requirements.
  • [security]: cookies, content disposition, MIME policy, URL upload restrictions, and rate limits.
  • [delivery]: cache behavior, file origin, isolated file origin, and signed internal URLs.
  • [smtp] and [oidc]: email and OIDC login.
  • [scanning]: command, webhook, or ClamAV upload scanners.
  • [processing]: metadata extraction, metadata stripping, and thumbnails.
  • [jobs], [metrics], [tokens], [moderation], and [uploads]: operational controls.

Runtime Settings

The admin settings UI persists selected sections to the settings table as JSON. When a request runs, Midden merges those persisted settings over the startup configuration, then reapplies only the exact runtime fields controlled by MIDDEN__ variables. File-only sections such as database and storage still come from startup configuration.

This means changing a TOML value does not automatically override a value already saved in the admin UI. Use the admin UI as the current source for runtime-adjustable settings unless an exact field is explicitly controlled by the environment.

Environment Variables

Environment variables use the prefix MIDDEN, double underscores for nesting, and names matching the TOML keys.

Examples

MIDDEN__SERVER__BIND=0.0.0.0:8080
MIDDEN__SERVER__PUBLIC_BASE_URL=https://files.example.test
MIDDEN__DATABASE__URL=postgres://midden:secret@postgres:5432/midden
MIDDEN__STORAGE__BACKEND=s3
MIDDEN__STORAGE__S3__BUCKET=midden
MIDDEN__SECURITY__SECURE_COOKIES=true
MIDDEN__METRICS__ENABLED=true

These map to:

[server]
bind = "0.0.0.0:8080"
public_base_url = "https://files.example.test"

[database]
url = "postgres://midden:secret@postgres:5432/midden"

[storage]
backend = "s3"

[storage.s3]
bucket = "midden"

[security]
secure_cookies = true

[metrics]
enabled = true

Lists And Tables

The config loader parses environment values when possible. For complex structures such as arrays, role quota maps, homepage blocks, scanner adapters, and rate limits, prefer TOML files unless your deployment tooling has a proven way to pass the equivalent structure.

Secrets

Keep these out of committed files:

  • MIDDEN__OIDC__CLIENT_SECRET
  • MIDDEN__SMTP__PASSWORD
  • MIDDEN__STORAGE__S3__SECRET_ACCESS_KEY
  • MIDDEN__DELIVERY__INTERNAL_URL_SECRET
  • MIDDEN__METRICS__BEARER_TOKEN
  • MIDDEN__MODERATION__NOTIFY_WEBHOOK_SECRET

Use your process manager, container orchestrator, or secret manager to inject them at runtime.

Storage

Midden stores metadata in the database and blob bytes in the configured object storage backend. File blobs are addressed by SHA-256 hash and stored using a two-level prefix layout.

Local Storage

[storage]
backend = "local"

[storage.local]
path = "data/blobs"

The local backend creates the directory if it does not exist. Back up this directory together with the database.

S3-Compatible Storage

[storage]
backend = "s3"

[storage.s3]
bucket = "midden"
region = "us-east-1"
endpoint = "https://s3.example.test"
access_key_id = "midden"
secret_access_key = "secret"
prefix = "production"
allow_http = false
virtual_hosted_style = false

bucket is required when storage.backend = "s3". endpoint, credentials, and style flags are optional because the underlying AWS client can also use environment or platform credentials.

Set allow_http = true only for local S3-compatible systems such as MinIO on a private network.

Verification

midden --config midden.toml storage verify

This compares blob hashes referenced by the database with hashes listed by the storage backend and reports missing or orphaned objects.

Garbage Collection

midden --config midden.toml storage gc --dry-run
midden --config midden.toml storage gc

Garbage collection expires due files and pastes, decrements blob references, and deletes unreferenced blob objects.

Run non-dry-run garbage collection only while every Midden server and job process that uses the same database and storage backend is stopped. The CLI cannot share the in-process upload lock, so running it alongside uploads could delete a content-addressed object as that hash is being reused. The dry run and storage verify remain safe while the service is online.

Background jobs perform similar cleanup automatically when enabled.

Databases

Midden supports SQLite and PostgreSQL through SQLx.

SQLite

Default:

[database]
url = "sqlite://midden.db?mode=rwc"
max_connections = 8

SQLite is good for small self-hosted deployments and development. Keep the database file on durable storage and back it up with the blob directory.

In containers, use an absolute path inside the mounted volume:

MIDDEN__DATABASE__URL=sqlite:///var/lib/midden/midden.db?mode=rwc

PostgreSQL

[database]
url = "postgres://midden:secret@postgres:5432/midden"
max_connections = 8

Use PostgreSQL for larger deployments, managed backup tooling, and external database operations.

Migrations

Apply migrations explicitly:

midden --config midden.toml migrate

midden serve also runs migrations at startup. Explicit migrations are useful in deployment pipelines because failures happen before the HTTP listener starts.

Runtime Settings Table

The settings table stores JSON runtime settings saved through the admin UI. Back up this table with the rest of the database, because it can contain important live policy and feature decisions that are not present in midden.toml.

Authentication

Midden supports local accounts, OIDC login, invite-based or open signup, API tokens, and two-factor email challenges.

Feature Switches

[features]
accounts = true
local_login = true
oidc_login = false

[policy]
signup = "disabled"
create_account = "disabled"

accounts controls account surfaces. local_login controls password login and registration affordances. oidc_login controls OIDC login routes, but OIDC must also be configured.

Signup Modes

policy.signup accepts:

  • disabled: no public signup.
  • open: public registration is available.
  • invite_only: users need invite tokens created by admins.
  • admin_created: admins create users manually.

Local Login

Local login uses password hashes stored on users. Owner password recovery is available from the CLI:

midden --config midden.toml owner reset-password --email owner@example.test --password new-password

OIDC Login

[features]
accounts = true
oidc_login = true

[oidc]
enabled = true
issuer_url = "https://accounts.example.test"
client_id = "midden"
client_secret = "secret"
redirect_url = "https://files.example.test/auth/oidc/callback"
allowed_domains = ["example.test"]
allowed_groups = ["midden-users"]
role_claim = "role"
groups_claim = "groups"

[oidc.role_mappings]
midden-moderators = "moderator"
midden-admins = "admin"

OIDC is considered usable only when accounts, the OIDC feature flag, provider config, client credentials, and redirect URL are present. The admin save path rejects settings that would disable local login without a usable OIDC login path.

Two-Factor Challenges

Users can enable two-factor authentication from the account page. Midden sends a challenge code by email, so SMTP must be configured for the challenge flow to be usable.

Roles

Roles are ordered:

user < moderator < admin < owner

Use the CLI to assign roles:

midden --config midden.toml user set-role --email user@example.test --role moderator

SMTP And Email

SMTP enables password resets, email verification, report notifications, and two-factor challenge delivery.

Configuration

[smtp]
enabled = true
host = "smtp.example.test"
port = 587
username = "midden"
password = "secret"
from = "Midden <midden@example.test>"

Midden considers mail enabled only when enabled = true, host is set, and from is set. Username and password are optional for SMTP servers that do not require authentication.

Uses

  • Password reset request emails.
  • Email verification links.
  • Two-factor challenge codes.
  • Abuse or report notifications when branding.abuse_email is configured.

Operator Notes

  • Use a real sender address that your SMTP service allows.
  • Prefer secret injection for smtp.password.
  • Test password reset and two-factor flows after changing SMTP settings.
  • If SMTP is not enabled, account flows that depend on mail cannot complete.

Security Policy

Midden exposes security controls through feature flags, action policies, content policy, URL upload restrictions, rate limits, delivery settings, and moderation states.

Action Rules

Action rules accept:

disabled
anonymous
authenticated
moderator
admin
owner

Example:

[policy]
upload_file = "anonymous"
create_paste = "anonymous"
use_api = "anonymous"
view_item = "anonymous"
delete_own_item = "authenticated"
delete_policy = "delete_tokens"
claim_anonymous_item = "authenticated"
create_account = "disabled"

Delete Policy

delete_policy accepts:

  • disabled: anonymous delete tokens cannot delete.
  • delete_tokens: anonymous delete tokens can delete.
  • no_anonymous_delete: only authorized account users can delete.
  • claim_later: anonymous items can be claimed by an account with the token.

Content Policy

[security.content_policy]
allowed_mime_types = []
forced_attachment_mime_types = ["image/svg+xml", "text/html", "application/javascript", "text/javascript"]
risky_mime_mode = "attachment"
max_filename_bytes = 180

If allowed_mime_types is empty, all MIME types are accepted unless blocked by scanner settings. Forced attachment types are served as downloads to reduce browser execution risk.

risky_mime_mode accepts:

  • attachment: serve risky types as attachments.
  • inline_on_isolated_origin: allow inline only on the isolated file origin.
  • plaintext: serve risky types as text/plain.

MIME Mismatch Rejection

[security]
reject_mime_mismatch = true

When enabled, Midden rejects uploads where sniffed content conflicts with the declared or filename-derived MIME type.

URL Upload Restrictions

URL upload blocks private and local IPs by default and supports allow/block lists for ports and hosts. Keep block_private_ips = true unless the instance is strictly internal and you understand the SSRF risk.

Rate Limits

Rate limits are disabled unless a named action is configured.

[security.rate_limits.login]
enabled = true
requests = 10
window_seconds = 300

Common action names include upload_file, upload_by_url, create_paste, login, password_reset, report, api_upload_file, api_create_paste, api_delete_file, api_delete_paste, api_create_token, api_create_report, api_list_files, and api_list_pastes.

Quotas And Limits

Midden applies upload size limits, paste size limits, expiry guardrails, and optional file storage quotas.

Size Limits

[limits]
max_upload_bytes = 2147483648
max_paste_bytes = 1048576

max_upload_bytes applies to multipart and URL uploads. max_paste_bytes applies to paste creation and paste edits.

Expiry Defaults

[limits]
default_file_expiry = "30d"
default_paste_expiry = "7d"

If defaults are omitted, users may create items without an expiry when allowed by guardrails.

Expiry values use compact durations such as 1h, 12h, 1d, 7d, and 30d.

Expiry Guardrails

[limits.expiry]
allow_never = true
anonymous_max_file_expiry = "7d"
user_max_file_expiry = "90d"
anonymous_max_paste_expiry = "7d"
user_max_paste_expiry = "90d"
allowed_presets = ["1h", "12h", "1d", "7d", "30d"]

Guardrails control which expiry choices are accepted and which presets are shown in forms.

Anonymous Quotas

[limits.anonymous_quota]
storage_bytes = 10737418240
daily_upload_bytes = 1073741824
monthly_upload_bytes = 10737418240
item_count = 1000

Anonymous quota applies to anonymous file uploads. Paste content does not consume file storage quota.

Role Quotas

[limits.role_quotas.user]
storage_bytes = 5368709120
daily_upload_bytes = 1073741824
monthly_upload_bytes = 10737418240
item_count = 500

Role quotas apply to account-owned file uploads. Configure only the limits you need; unset values are unlimited.

Delivery And CDN

Delivery settings control public URLs, cache headers, isolated file serving, and signed internal raw URLs.

Base URLs

[server]
public_base_url = "https://files.example.test"

[delivery]
public_file_base_url = "https://cdn-files.example.test"

server.public_base_url is the application origin. delivery.public_file_base_url, when set, is used for file URLs and can point at a separate file domain or CDN.

Cache Settings

[delivery]
public_cache_seconds = 3600
static_cache_seconds = 31536000

Static assets can be cached longer than user files. Tune public file cache TTLs based on your moderation and takedown expectations.

Isolated File Origin

[delivery]
isolated_file_origin = true
public_file_base_url = "https://files-cdn.example.test"

When isolated file origin is enabled, public file routes are only available through the configured file host. This reduces the risk of user-controlled file content sharing the main application origin.

Signed Internal URLs

[delivery]
signed_internal_urls = true
internal_url_secret = "long-random-secret"
internal_url_ttl_seconds = 300

Signed internal raw URLs are included in API file responses when enabled. Use them for trusted reverse proxy or CDN fetches that need short-lived origin access.

Midden validates startup config so signed internal URLs require internal_url_secret, and isolated file origin requires public_file_base_url.

Reverse Proxies

[server]
behind_proxy = true

When enabled, access checks that need the client IP can use x-real-ip or the first x-forwarded-for value. Only enable this behind a trusted proxy that strips untrusted incoming forwarding headers.

Scanning

Midden can scan uploads before they are published. Scanners return one of three decisions:

  • allow: publish normally.
  • quarantine: store the file and scanner result, but do not serve it publicly.
  • reject: reject the upload.

If multiple adapters run, Midden uses the most restrictive decision.

Enable Scanning

[scanning]
enabled = true
blocked_hashes = []
blocked_mime_types = []
default_on_error = "allow"

default_on_error controls what happens when an adapter fails. For high-trust public upload systems, consider quarantine instead of allow.

Block Lists

[scanning]
blocked_hashes = ["012345..."]
blocked_mime_types = ["application/x-msdownload"]

Blocked hashes and MIME types are checked before external adapters.

Command Adapter

[[scanning.adapters]]
kind = "command"
program = "/usr/local/bin/midden-scan"
args = ["{path}", "{filename}", "{content_type}", "{sha256}"]

Command exit codes:

  • 0: allow.
  • 10: quarantine.
  • 20: reject.
  • Any other status or execution error: use default_on_error.

Midden expands {path}, {filename}, {content_type}, {sha256}, {public_id}, and {size_bytes} in command arguments.

Webhook Adapter

[[scanning.adapters]]
kind = "webhook"
url = "https://scanner.example.test/midden"
secret = "change-me"

Midden posts JSON metadata to the webhook. When secret is set, it sends it in the x-midden-scanner-secret header.

Expected response:

{
  "decision": "allow",
  "detail": "clean"
}

ClamAV Adapter

[[scanning.adapters]]
kind = "clam_av"
socket = "127.0.0.1:3310"

The socket can be TCP (host:port) or a Unix socket path on Unix platforms.

Background Retries

Background jobs retry scanner decisions for candidate files when scanning is enabled. Configure retry batch size with:

[jobs]
scanner_retry_limit = 10

Moderation

Moderation features include reports, moderation roles, item states, notes, admin search, blocked hashes, and optional report webhooks.

Feature Flag

[features]
reports = true

When reports are disabled, report forms and report APIs are unavailable.

Roles

  • moderator: can use moderation search, reports, and moderation item actions.
  • admin: includes moderator access and user/settings management.
  • owner: includes admin access and owner-only account mutation safeguards.

Item States

Moderation can set files and pastes to:

active
quarantined
takedown
legal_hold
deleted

Non-active items render the configured takedown page text instead of serving normal content.

Reports

Users and anonymous visitors can submit reports when reports are enabled. Reports capture item kind, public ID, reason, details, optional reporter user, and state.

Admin and moderator surfaces support filtering by report state, kind, reason, and age.

Moderation Webhook

[moderation]
notify_webhook_url = "https://moderation.example.test/midden"
notify_webhook_secret = "change-me"

When configured, Midden sends report notifications to the webhook. Keep the secret out of committed configuration.

Abuse Email

[branding]
abuse_email = "abuse@example.test"

When SMTP is enabled, reports also send an email notification to this address.

Block Hash From Item

Admin item actions can add a file blob hash to scanning.blocked_hashes. This only applies to files, not pastes.

Jobs And Maintenance

Midden runs background jobs while serving and exposes a one-shot job command for operators.

Configuration

[jobs]
enabled = true
interval_seconds = 300
metadata_limit = 25
scanner_retry_limit = 10
storage_verify_interval_seconds = 3600

The runtime enforces a minimum sleep interval of 30 seconds.

Job Work

Each pass can:

  • Expire due files and delete unreferenced blobs.
  • Expire due pastes.
  • Clean expired sessions, password reset tokens, email verification tokens, two-factor challenges, invite tokens, and OIDC auth state.
  • Retry scanner decisions for candidate files.
  • Extract file metadata.
  • Create thumbnail derivatives.
  • Verify database blob references against backend object storage.

Run Once

midden --config midden.toml jobs run-once

The command prints a summary:

jobs complete: expired_files=0, expired_pastes=0, expired_auth_rows=0, deleted_blobs=0, deleted_temp_files=0, scanner_retries=0, metadata_updates=0, missing_blobs=0, orphaned_blobs=0

The admin jobs page exposes the same one-shot run for admins.

The in-process background, admin, and one-shot job paths serialize blob cleanup with uploads. By contrast, storage gc is a separate maintenance process and must be run with all Midden server and job processes stopped; see Storage.

Storage Drift

When storage verification finds missing or orphaned blobs, Midden logs a warning and includes counts in the job summary. Use storage verify for a direct operator check.

Metrics And Health

Midden exposes health endpoints and optional Prometheus/OpenMetrics metrics.

Health

GET /healthz
GET /readyz

/healthz returns ok when the HTTP server is alive.

/readyz checks database and storage health and returns:

database=true
storage=true

If either dependency is unavailable, it returns HTTP 503 with the failed dependency state.

Metrics

[metrics]
enabled = true
access = "admin"

Metrics are served at:

GET /metrics

The response content type is OpenMetrics text.

Access Modes

metrics.access accepts:

  • public: no authentication.
  • admin: current web session must be an admin or owner.
  • token: request must include Authorization: Bearer <metrics bearer token>.
  • loopback: request client IP must be loopback.

Token mode requires:

[metrics]
access = "token"
bearer_token = "change-me"

Loopback mode can respect reverse proxy headers only when server.behind_proxy = true.

Metric Names

Registered metrics include:

  • uploads
  • pastes
  • upload_bytes
  • served_files
  • reports
  • scanner_outcomes
  • rate_limit_rejections
  • request_latency_seconds

Backup And Restore

Back up the database and blob storage together. The database contains metadata, settings, sessions, API tokens, reports, audit events, scanner results, and blob reference counts. Storage contains the actual file bytes and generated derivatives.

SQLite With Local Storage

Stop the service or take a consistent filesystem snapshot, then copy:

midden.db
data/blobs/

If running in Docker Compose with the SQLite file under /var/lib/midden, back up the midden-data volume.

PostgreSQL With S3

Use PostgreSQL-native backup tooling for the database and bucket-native backup or replication for object storage.

Confirm both systems are from a compatible point in time. A database backup without matching blobs can leave missing storage objects. Blob backups without matching database rows can leave orphaned objects.

Blob Export And Import

Midden can export blobs referenced by the current database:

midden --config midden.toml storage export ./midden-blob-export

The export contains:

manifest.json
blobs/<hash>

Import blobs into the configured storage backend:

midden --config midden.toml storage import ./midden-blob-export

This imports blob bytes only. It does not restore database rows. Use it with database backup and restore procedures.

Verify After Restore

midden --config midden.toml migrate
midden --config midden.toml storage verify
midden --config midden.toml config check

Troubleshooting

Configuration Fails To Load

Run:

midden --config midden.toml config check

Common causes:

  • Explicit --config path does not exist.
  • Invalid enum values such as an unknown action rule or delete policy.
  • delivery.isolated_file_origin = true without delivery.public_file_base_url.
  • delivery.signed_internal_urls = true without delivery.internal_url_secret.
  • S3 backend selected without storage.s3.bucket.

Uploads Are Rejected

Check:

  • limits.max_upload_bytes.
  • security.content_policy.allowed_mime_types.
  • security.reject_mime_mismatch.
  • scanning.blocked_hashes.
  • scanning.blocked_mime_types.
  • Scanner adapter status and default_on_error.
  • User or anonymous quotas.

Valid Media Shows As application/octet-stream

Midden sniffs common formats, then falls back to declared MIME type, filename extension, and finally application/octet-stream. Make sure clients send a useful filename or content type when the bytes cannot be sniffed.

URL Upload Cannot Fetch A URL

Check:

  • features.upload_by_url.
  • URL scheme is http or https.
  • security.url_upload.block_private_ips.
  • Allowed or blocked host lists.
  • Allowed or blocked port lists.
  • Connect and request timeouts.
  • security.url_upload.max_response_bytes.

Check:

  • features.accounts.
  • features.local_login.
  • features.oidc_login.
  • policy.signup.
  • Full OIDC provider configuration if local login is disabled.

Metrics Return 403 Or 404

  • 404: metrics.enabled is false.
  • 403 in admin mode: request is not from an admin session.
  • 403 in token mode: missing or mismatched bearer token.
  • 403 in loopback mode: request IP is not loopback or proxy headers are not trusted.

Public Browse Is Empty

Public browse only lists items with visibility = "public" and requires:

[features]
public_browse = true

Unlisted items are reachable by direct link but do not appear in browse.

Files

The home page is the primary file upload surface.

Upload

The upload form accepts:

  • file: the file bytes.
  • expires: optional expiry duration.
  • visibility: unlisted, private, or public when public browse is enabled.

Anonymous uploads are allowed by default. If policy requires authentication, sign in first.

Results

A successful upload returns:

  • A page URL.
  • A raw file URL.
  • A delete token for anonymous uploads when the delete policy supports it.

Keep delete tokens private. They can delete or claim anonymous items depending on policy.

Visibility

  • unlisted: reachable by direct link.
  • private: visible only to the owning account and moderators.
  • public: visible in /browse when public browse is enabled.

Preview Pages

When features.preview_pages = true, file links open a preview page first. Otherwise, file links serve the raw file directly.

URL Upload

When features.upload_by_url = true, /url-upload lets users fetch a remote http or https URL into Midden. Operators can restrict hosts, ports, redirects, response size, and private IP access.

Pastes

Pastes are text items with optional titles, syntax hints, expiry, visibility, ownership, and revisions.

Create A Paste

Open:

/p/new

The paste form accepts:

  • title: optional display title.
  • syntax: optional syntax hint.
  • content: required paste body.
  • expires: optional expiry duration.
  • visibility: unlisted, private, or public when public browse is enabled.

View A Paste

Paste pages are served at:

/p/{id}
/p/{id}/raw

The normal page renders syntax-highlighted content when a syntax hint is available. The raw route serves plain paste content.

Edit A Paste

Paste editing requires features.paste_editing = true. Owners can edit their own pastes. Admins can edit pastes as part of moderation.

Each edit creates a revision record.

Limits

Paste creation and edit size are limited by:

[limits]
max_paste_bytes = 1048576

Accounts And Tokens

Accounts let users own files and pastes, search their items, manage visibility and expiry, create API tokens, link OIDC identities, and configure security features.

Account Page

Open:

/account

The account page shows owned files, owned pastes, API tokens when the API is enabled, password controls for local accounts, email verification state, two-factor state, and OIDC linking when available.

Bulk Item Actions

Account-owned files and pastes support bulk actions:

  • Delete selected items.
  • Set visibility.
  • Set expiry.

Authorization still checks ownership and delete policy.

API Tokens

API tokens start with mdd_ and are shown once at creation time. Store them securely.

Token creation requires at least one scope. Operators can configure default and maximum TTLs:

[tokens]
default_ttl_seconds = 2592000
max_ttl_seconds = 31536000

Two-Factor Authentication

Two-factor setup uses emailed challenge codes. SMTP must be configured and working.

OIDC Linking

When OIDC is enabled, signed-in users can link an OIDC identity from the account page.

Reports Claims And Deletes

Reports

When reports are enabled, users can report files and pastes from item pages:

/report/file/{id}
/report/paste/{id}

Reports include a reason and optional details. Operators can notify a moderation webhook and an abuse email address.

Deletes

Delete forms are available at:

/delete/file/{id}
/delete/paste/{id}

Authorized account owners can delete their own items when policy allows. Anonymous delete depends on the delete token and the configured delete policy.

Claims

Claim forms are available at:

/claim/file/{id}
/claim/paste/{id}

Claims let an authenticated user attach an anonymous item to their account using the item delete token. This requires policy.claim_anonymous_item to allow the signed-in user.

Unavailable Items

Deleted, quarantined, takedown, and legal hold items render the unavailable item page instead of normal content.

API Overview

Midden exposes JSON and multipart endpoints under /api/v1.

The runtime OpenAPI document is available at:

GET /api/openapi.json

The OpenAPI output is the machine-readable reference for the running version. This guide provides practical usage notes and examples.

Enable API

[features]
api = true

[policy]
use_api = "anonymous"

Individual endpoints still check feature flags, policies, scopes, roles, and rate limits.

Common Response Fields

File item responses can include:

  • id
  • url
  • raw_url
  • internal_url
  • thumbnail_url
  • filename
  • content_type
  • size_bytes
  • image_width
  • image_height
  • visibility
  • metadata
  • expires_at
  • state
  • created_at

Paste item responses can include:

  • id
  • url
  • raw_url
  • title
  • syntax
  • size_bytes
  • visibility
  • expires_at
  • state
  • created_at

Errors

API routes use standard HTTP status codes such as 400, 401, 403, 404, 413, 429, and 500. Feature-disabled API routes return 403 or 404 depending on the surface.

Authentication And Scopes

API tokens are bearer tokens:

curl -H 'authorization: Bearer mdd_TOKEN' http://127.0.0.1:8080/api/v1/me/files

Some API routes can be anonymous when policy allows it. Account-specific routes and token management routes require a token.

Scopes

Common scopes:

files:read
files:write
files:delete
pastes:read
pastes:write
pastes:delete
reports:write
items:claim
tokens:read
tokens:write
admin:reports
admin:items
admin:search
*

* grants all scopes to the token holder. Token creation through the API can only request scopes already held by the caller unless the caller has *.

Create A Token From The Account UI

Open /account, choose API Tokens, enter a name, scopes, and optional TTL seconds.

Token Expiry

Operators can set default and maximum TTLs:

[tokens]
default_ttl_seconds = 2592000
max_ttl_seconds = 31536000

API token creation rejects non-positive TTLs and TTLs above the configured maximum.

Files API

Upload A File

curl -F file=@example.txt \
  -F expires=7d \
  -F visibility=unlisted \
  http://127.0.0.1:8080/api/v1/files

Authenticated upload:

curl -H 'authorization: Bearer mdd_TOKEN' \
  -F file=@example.txt \
  http://127.0.0.1:8080/api/v1/files

Required scope for authenticated callers:

files:write

Response:

{
  "id": "abc123",
  "url": "http://127.0.0.1:8080/abc123.txt",
  "raw_url": "http://127.0.0.1:8080/files/abc123/raw",
  "internal_url": null,
  "delete_token": "token"
}

List My Files

curl -H 'authorization: Bearer mdd_TOKEN' \
  'http://127.0.0.1:8080/api/v1/me/files?q=example'

Required scope:

files:read

Response:

{
  "items": []
}

Delete A File

curl -X DELETE \
  -H 'authorization: Bearer mdd_TOKEN' \
  http://127.0.0.1:8080/api/v1/files/abc123

Required scope:

files:delete

Anonymous delete token:

curl -X DELETE \
  -H 'x-delete-token: DELETE_TOKEN' \
  http://127.0.0.1:8080/api/v1/files/abc123

Response:

{
  "deleted": true
}

Claim A File

curl -H 'authorization: Bearer mdd_TOKEN' \
  -H 'content-type: application/json' \
  -d '{"delete_token":"DELETE_TOKEN"}' \
  http://127.0.0.1:8080/api/v1/claim/file/abc123

Required scope:

items:claim

Pastes API

Create A Paste

curl -H 'content-type: application/json' \
  -d '{"title":"Example","syntax":"rust","content":"fn main() {}","expires":"7d","visibility":"unlisted"}' \
  http://127.0.0.1:8080/api/v1/pastes

Authenticated create:

curl -H 'authorization: Bearer mdd_TOKEN' \
  -H 'content-type: application/json' \
  -d '{"content":"hello"}' \
  http://127.0.0.1:8080/api/v1/pastes

Required scope for authenticated callers:

pastes:write

Response:

{
  "id": "abc123",
  "url": "http://127.0.0.1:8080/p/abc123",
  "raw_url": "http://127.0.0.1:8080/p/abc123/raw",
  "delete_token": "token"
}

List My Pastes

curl -H 'authorization: Bearer mdd_TOKEN' \
  'http://127.0.0.1:8080/api/v1/me/pastes?q=rust'

Required scope:

pastes:read

features.paste_content_search controls whether owned paste search includes paste body content.

Delete A Paste

curl -X DELETE \
  -H 'authorization: Bearer mdd_TOKEN' \
  http://127.0.0.1:8080/api/v1/pastes/abc123

Required scope:

pastes:delete

Anonymous delete token:

curl -X DELETE \
  -H 'x-delete-token: DELETE_TOKEN' \
  http://127.0.0.1:8080/api/v1/pastes/abc123

Response:

{
  "deleted": true
}

Claim A Paste

curl -H 'authorization: Bearer mdd_TOKEN' \
  -H 'content-type: application/json' \
  -d '{"delete_token":"DELETE_TOKEN"}' \
  http://127.0.0.1:8080/api/v1/claim/paste/abc123

Required scope:

items:claim

Reports And Moderation API

Create A Report

curl -H 'content-type: application/json' \
  -d '{"kind":"file","id":"abc123","reason":"abuse","details":"details"}' \
  http://127.0.0.1:8080/api/v1/reports

Authenticated report:

curl -H 'authorization: Bearer mdd_TOKEN' \
  -H 'content-type: application/json' \
  -d '{"kind":"paste","id":"abc123","reason":"spam"}' \
  http://127.0.0.1:8080/api/v1/reports

Required scope for authenticated callers:

reports:write

Response:

{
  "reported": true
}

List Reports

curl -H 'authorization: Bearer mdd_TOKEN' \
  'http://127.0.0.1:8080/api/v1/admin/reports?state=open&kind=file&days=7'

Required scope and role:

admin:reports
moderator or higher

Update A Report

curl -X PATCH \
  -H 'authorization: Bearer mdd_TOKEN' \
  -H 'content-type: application/json' \
  -d '{"action":"resolve","note":"handled"}' \
  http://127.0.0.1:8080/api/v1/admin/reports/REPORT_ID

The JSON body matches the admin report action form fields: action and optional note.

Update An Item

curl -X PATCH \
  -H 'authorization: Bearer mdd_TOKEN' \
  -H 'content-type: application/json' \
  -d '{"state":"takedown","visibility":"unlisted","note":"confirmed","block_hash":true}' \
  http://127.0.0.1:8080/api/v1/admin/items/file/abc123

Required scope and role:

admin:items
moderator or higher

Valid item states:

active
quarantined
takedown
legal_hold
deleted

deleted is terminal for files. Once a file is deleted or expired, its blob reference may be released and the object removed, so it cannot be changed back to a live moderation state.

block_hash only works for files.

curl -H 'authorization: Bearer mdd_TOKEN' \
  'http://127.0.0.1:8080/api/v1/admin/search?q=example&paste_content=true'

Required scope and role:

admin:search
moderator or higher

Tokens API

List Tokens

curl -H 'authorization: Bearer mdd_TOKEN' \
  http://127.0.0.1:8080/api/v1/tokens

Required scope:

tokens:read

Response:

{
  "items": []
}

Create Token

curl -H 'authorization: Bearer mdd_TOKEN' \
  -H 'content-type: application/json' \
  -d '{"name":"automation","scopes":["files:read","files:write"],"expires_in_seconds":2592000}' \
  http://127.0.0.1:8080/api/v1/tokens

Required scope:

tokens:write

Response:

{
  "token": "mdd_NEW_TOKEN",
  "expires_at": 1754093490
}

The token is only returned once. Store it before leaving the response.

The requested scopes must be a subset of the caller token scopes unless the caller token has *.

Revoke Token

curl -X DELETE \
  -H 'authorization: Bearer mdd_TOKEN' \
  http://127.0.0.1:8080/api/v1/tokens/TOKEN_ID

Required scope:

tokens:write

Response:

{
  "revoked": true
}

Architecture

Midden is a single Rust binary with focused modules around configuration, state, storage, database access, processing, web routing, jobs, metrics, and mail.

Entry Point

src/main.rs defines the CLI:

  • serve
  • migrate
  • config check
  • config print-defaults
  • owner create
  • owner reset-password
  • storage gc
  • storage verify
  • storage export
  • storage import
  • jobs run-once
  • user set-role

serve builds AppState, runs migrations, starts background jobs, builds the router, and listens with graceful shutdown.

App State

src/app.rs builds shared state:

  • Parsed AppConfig.
  • Database.
  • BlobStorage.
  • Built-in or disk templates.
  • Mailer.
  • Metrics registry.
  • Rate limiter.
  • Upload quota lock.

Handlers call state.settings().await to load runtime settings merged from config and the database.

Web Layer

src/web.rs owns router registration and cross-cutting middleware. Handler modules live under src/web/:

  • auth.rs
  • account.rs
  • files.rs
  • pastes.rs
  • items.rs
  • browse.rs
  • admin.rs
  • api.rs
  • system.rs
  • upload.rs
  • oidc.rs
  • support.rs

Keep route registration centralized unless there is a clear reason to split it further. The /{slug} file route is a catch-all and must remain after more specific routes.

Persistence

src/db.rs and src/db/ contain schema, models, auth, item, moderation, search, and settings methods. The schema string creates tables for settings, users, sessions, API tokens, auth flows, blobs, files, pastes, revisions, reports, scanner results, audit events, rate-limit buckets, and moderation notes.

Routing And Templates

Router

Routes are registered in src/web.rs. Public app routes, API routes, account routes, admin routes, system routes, and file routes are merged into one Axum router.

Important route groups:

  • /, /url-upload, /browse
  • /files/{id}/raw, /files/{id}/thumbnail, /internal/files/{id}/raw, /{slug}
  • /p/new, /p/{id}, /p/{id}/raw, /p/{id}/edit
  • /auth/*, /register, /account
  • /admin/*
  • /api/docs, /api/openapi.json, /api/v1/*
  • /healthz, /readyz, /metrics, /robots.txt

Middleware

The router layers:

  • API error middleware.
  • File origin middleware.
  • Request context middleware.
  • Request metrics middleware.
  • CSRF cookie middleware.
  • HTTP tracing.

Request context stores templates, runtime settings, current user, CSRF token, and HTMX state for rendering and error handling.

Templates

Templates live in templates/ and are registered in src/templates.rs with include_str!. If you add a built-in template, also register it there.

Operators can override templates with:

[server]
template_dir = "/etc/midden/templates"

Disk templates take precedence over built-ins when present.

Static Assets

Built-in static assets live in static/ and are served from /static/{path}. Operators can override static files with:

[server]
static_dir = "/etc/midden/static"

Disk static files take precedence over built-ins.

Runtime Settings

Startup configuration is represented by AppConfig in src/config.rs. Runtime request settings are represented by RuntimeSettings.

Loading

AppConfig::load reads:

  1. Compiled defaults.
  2. Explicit config file or optional midden.toml.
  3. MIDDEN__ environment variables, while recording the exact runtime field paths they control.
  4. Validation rules.

AppState::settings() asks the database for runtime settings, merges persisted JSON settings over the pre-environment startup baseline, and then reapplies the recorded environment-controlled fields. The effective runtime precedence is defaults/file < persisted database settings < explicit MIDDEN__ fields. Admin saves restore environment-owned fields from the raw persisted settings before writing, so a temporary environment override never becomes permanent as a side effect of another edit.

Environment locking is field-level rather than section-level. If MIDDEN__SECURITY__SECURE_COOKIES is present, only security.secure_cookies comes from the environment; sibling fields in the persisted security object remain active. If the environment variable is removed, the value already persisted for that field becomes active.

Persisted Sections

Admin settings can persist runtime sections such as:

  • Features.
  • Limits.
  • Branding.
  • Policy.
  • Security.
  • Delivery.
  • Scanning.
  • Processing.
  • Discovery.
  • Jobs.
  • Uploads.
  • Metrics.
  • Tokens.
  • Moderation.

Database and storage connection settings remain startup configuration because changing them at runtime would require rebuilding application state.

Adding A Runtime Setting

When adding a runtime-adjustable option:

  1. Add it to the appropriate config struct with serde defaults.
  2. Include it in RuntimeSettings.
  3. Merge it in RuntimeSettings::from_config. If it introduces a new top-level runtime group, add that group once to the runtime_setting_groups! descriptor; loading, atomic persistence, and environment-path ownership all use that descriptor.
  4. Update admin form parsing and template fields if it should be user-editable.
  5. Persist production admin changes through replace_runtime_settings; set_json_setting is a test-only fixture helper.
  6. Validate both startup and live values through the shared validate_runtime_settings rules.
  7. Add route-level or integration tests for the rendered admin form, atomic save path, and environment-precedence behavior when it can drift.

Validation

Use AppConfig::validate for startup invariants that must fail before serving. Put runtime invariants in validate_runtime_settings so startup, database loading, and the admin save path enforce the same rules.

Uploads And Processing

Upload behavior is split between route handlers, multipart reading, content resolution, scanner integration, persistence, and background processing.

Upload Flow

  1. Route handler checks feature flags, policy, CSRF when applicable, and rate limits.
  2. Multipart or URL upload is read to a temporary file.
  3. Content type is resolved from sniffed bytes, declared content type, filename extension, then application/octet-stream.
  4. MIME policy, filename length, blocked hashes, blocked MIME types, and quota are checked.
  5. Optional metadata stripping rewrites JPEG or PNG uploads.
  6. SHA-256 hash is computed.
  7. External scanners run.
  8. Blob row and file item row are created.
  9. Blob bytes are written if the object does not already exist.
  10. Scanner results are recorded.
  11. Active uploads return URLs; quarantined or rejected uploads return errors.

URL Upload

URL upload validates scheme, host, ports, redirects, timeouts, response size, and private IP behavior before reading the response.

Processing

src/processing.rs handles MIME sniffing, metadata JSON, metadata stripping helpers, image dimensions, and thumbnail derivatives.

Background jobs fill missing metadata and thumbnails for existing files when configured:

[processing]
metadata_extraction = true
thumbnails = true

Storage

src/storage.rs wraps object_store for local and S3-compatible backends. Blob paths are derived from sanitized hex hashes and optional S3 prefixes.

Tests

Upload-related changes should include focused tests for resolver behavior plus route-level tests when policies, forms, or API behavior changes.

Tests And Release

Common Checks

Run formatting:

cargo fmt --all -- --check

Run clippy:

cargo clippy --locked --workspace --all-targets --all-features -- -D warnings

Run tests:

cargo test --locked --workspace --all-features

Run docs checks:

mdbook build docs
mdbook test docs

Validate both Compose models:

docker compose -f docker-compose.yml -f docker-compose.sqlite.yml config --quiet
docker compose -f docker-compose.yml -f docker-compose.postgres-minio.yml config --quiet

PostgreSQL and S3 integration tests are ignored by the default suite because they require external services. Supply every listed environment variable and invoke each test explicitly; the tests fail instead of silently skipping when their environment is incomplete:

MIDDEN_TEST_POSTGRES_URL=postgres://midden:midden@localhost:5432/midden \
  cargo test --locked db::tests::postgres_migration_smoke_when_configured -- \
  --ignored --exact --nocapture

MIDDEN_TEST_S3_BUCKET=midden \
MIDDEN_TEST_S3_REGION=us-east-1 \
MIDDEN_TEST_S3_ENDPOINT=http://127.0.0.1:9000 \
MIDDEN_TEST_S3_ACCESS_KEY_ID=midden \
MIDDEN_TEST_S3_SECRET_ACCESS_KEY=midden-secret \
  cargo test --locked storage::tests::s3_storage_round_trip_when_configured -- \
  --ignored --exact --nocapture

Check whitespace before committing:

git diff --check

Focused Test Guidance

  • Config changes: add unit tests in src/config.rs and route/admin tests if runtime settings are involved.
  • Route behavior: add integration-style tests in src/web/tests.rs.
  • Upload processing: add focused tests in src/web/upload.rs or src/processing.rs, then route tests when user-visible behavior changes.
  • Storage: add local backend tests and gated S3 smoke tests when touching S3-specific behavior.
  • Auth/config regressions: test rendered links and the admin save path.

Release Basics

For a crate release:

  1. Update Cargo.toml.
  2. Update Cargo.lock through a build or metadata command.
  3. Build with cargo build.
  4. Commit the version bump.
  5. Create an annotated tag such as git tag -a v0.6.5 -m "Release v0.6.5".
  6. Push the branch and tags.
  7. Confirm git status --short is clean.

Docker image publishing is handled by the GitHub workflow on main pushes and version tags.