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
middenbinary 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_storestores 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_URLto the public HTTPS origin. - Use durable volumes or managed services for the database and blob storage.
- Set
MIDDEN__SECURITY__SECURE_COOKIES=truebehind HTTPS. - Set
MIDDEN__SERVER__BEHIND_PROXY=truewhen 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:
- Compiled defaults.
- An explicit
--config PATH, which must exist, or the optionalmidden.tomlwhen no path is provided. - Persisted admin settings for runtime-adjustable sections.
- 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_SECRETMIDDEN__SMTP__PASSWORDMIDDEN__STORAGE__S3__SECRET_ACCESS_KEYMIDDEN__DELIVERY__INTERNAL_URL_SECRETMIDDEN__METRICS__BEARER_TOKENMIDDEN__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_emailis 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 includeAuthorization: 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:
uploadspastesupload_bytesserved_filesreportsscanner_outcomesrate_limit_rejectionsrequest_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
--configpath does not exist. - Invalid enum values such as an unknown action rule or delete policy.
delivery.isolated_file_origin = truewithoutdelivery.public_file_base_url.delivery.signed_internal_urls = truewithoutdelivery.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
httporhttps. 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.
Login Links Are Missing
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.enabledis false. - 403 in
adminmode: request is not from an admin session. - 403 in
tokenmode: missing or mismatched bearer token. - 403 in
loopbackmode: 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, orpublicwhen 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/browsewhen 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, orpublicwhen 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:
idurlraw_urlinternal_urlthumbnail_urlfilenamecontent_typesize_bytesimage_widthimage_heightvisibilitymetadataexpires_atstatecreated_at
Paste item responses can include:
idurlraw_urltitlesyntaxsize_bytesvisibilityexpires_atstatecreated_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.
Admin Search
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:
servemigrateconfig checkconfig print-defaultsowner createowner reset-passwordstorage gcstorage verifystorage exportstorage importjobs run-onceuser 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.rsaccount.rsfiles.rspastes.rsitems.rsbrowse.rsadmin.rsapi.rssystem.rsupload.rsoidc.rssupport.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:
- Compiled defaults.
- Explicit config file or optional
midden.toml. MIDDEN__environment variables, while recording the exact runtime field paths they control.- 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:
- Add it to the appropriate config struct with serde defaults.
- Include it in
RuntimeSettings. - Merge it in
RuntimeSettings::from_config. If it introduces a new top-level runtime group, add that group once to theruntime_setting_groups!descriptor; loading, atomic persistence, and environment-path ownership all use that descriptor. - Update admin form parsing and template fields if it should be user-editable.
- Persist production admin changes through
replace_runtime_settings;set_json_settingis a test-only fixture helper. - Validate both startup and live values through the shared
validate_runtime_settingsrules. - 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
- Route handler checks feature flags, policy, CSRF when applicable, and rate limits.
- Multipart or URL upload is read to a temporary file.
- Content type is resolved from sniffed bytes, declared content type, filename extension, then
application/octet-stream. - MIME policy, filename length, blocked hashes, blocked MIME types, and quota are checked.
- Optional metadata stripping rewrites JPEG or PNG uploads.
- SHA-256 hash is computed.
- External scanners run.
- Blob row and file item row are created.
- Blob bytes are written if the object does not already exist.
- Scanner results are recorded.
- 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.rsand 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.rsorsrc/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:
- Update
Cargo.toml. - Update
Cargo.lockthrough a build or metadata command. - Build with
cargo build. - Commit the version bump.
- Create an annotated tag such as
git tag -a v0.6.5 -m "Release v0.6.5". - Push the branch and tags.
- Confirm
git status --shortis clean.
Docker image publishing is handled by the GitHub workflow on main pushes and version tags.