Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 117 additions & 45 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,69 +1,141 @@
# FileForge Configuration
# Copy this file to .env and adjust values
# FileMorph Configuration
# Copy this file to .env and adjust values for your deployment.
#
# This file is structured top-down: required for every deployment first,
# then Cloud-overlay (account / payment / email features), then Compliance-
# Edition tunables, then operational knobs. A Community-Edition self-host
# only needs the first section.
#
# ──────────────────────────────────────────────────────────────────────
# === Required for every deployment ===
# ──────────────────────────────────────────────────────────────────────

# Server
# Server bind. APP_HOST=0.0.0.0 makes the app listen on every interface
# inside the container; the reverse proxy (Caddy/nginx) is what limits
# external reach. APP_DEBUG=true enables FastAPI's interactive docs at
# /docs and verbose tracebacks — leave off in production.
APP_HOST=0.0.0.0
APP_PORT=8000
APP_DEBUG=false

# Security
# Path to the JSON file storing hashed API keys
# Path to the JSON file holding hashed API keys for anonymous-tier
# clients. The file is created on first key generation; bind-mount the
# parent directory into your container if you want keys to survive
# restarts.
API_KEYS_FILE=data/api_keys.json

# Limits
# Maximum HTTP request body size accepted by the upload endpoints.
# 100 MB is a sane default that fits typical convert/compress workloads
# without inviting OOM on small hosts. Operators with bigger files raise
# this; tier quotas (anonymous/free/pro/business) still apply on top via
# app/core/quotas.py.
MAX_UPLOAD_SIZE_MB=100

# CORS: comma-separated list of allowed origins (* for all)
# CORS: comma-separated list of allowed origins. `*` is fine for dev or
# a single-origin same-domain deployment. Production deployments behind
# a real domain should list explicit origins (e.g.
# `https://files.example.com,https://www.files.example.com`) so a
# malicious page on a different domain can't talk to your API.
CORS_ORIGINS=*

# Optional: route heavy upload POSTs (convert/compress, single + batch) through
# a separate subdomain. Empty string = same-origin (default, simplest). Set
# only when the main site sits behind a proxy that caps request bodies and
# uploads must bypass it. See docs/self-hosting.md for CORS implications.
# Public canonical URL of the deployment. Used by the canonical/og:url
# meta tags, the sitemap, and the JSON-LD structured data. Localhost is
# fine for dev; set to your domain for production so search engines
# index the right URLs.
APP_BASE_URL=http://localhost:8000

# Optional: route heavy upload POSTs (convert/compress, single + batch)
# through a separate subdomain. Empty string = same-origin (default,
# simplest). Set this only when the main site sits behind a proxy that
# caps request bodies and uploads must bypass it via a tunnel
# subdomain. See docs/self-hosting.md for CORS implications.
API_BASE_URL=

# Public canonical URL for the deployment — used for canonical/og:url meta
# tags, sitemap entries, and JSON-LD structured data. Default localhost is
# fine for dev; set to your domain for prod (e.g. https://files.example.com).
APP_BASE_URL=http://localhost:8000
# ──────────────────────────────────────────────────────────────────────
# === Cloud-overlay (optional — accounts, payments, transactional email) ===
# ──────────────────────────────────────────────────────────────────────
# A Community Edition deployment can leave everything below empty.
# Setting any of these activates the corresponding sub-processor (see
# docs/sub-processors.md). The application disables each feature
# automatically when its primary key/url is empty — no further toggle
# needed.

# JWT signing secret. Required for the user-account features
# (registration, login, refresh, role checks). Must be at least 32
# characters; rotate by changing this value (all sessions invalidate
# on the next request).
JWT_SECRET=dev-secret-change-me-min-32-chars-long

# Whether to expose /pricing as a commercial offer surface. Self-hosted
# Community deployments leave this off — there's no commercial offer to
# advertise. Set to `true` only on a SaaS deployment that has (or will
# soon have) Stripe configured. The page renders a "Coming Soon" banner
# automatically when STRIPE_SECRET_KEY is empty.
# PostgreSQL DSN for the Cloud overlay (users, api_keys, file_jobs,
# audit_events tables). Use the asyncpg driver. Empty string disables
# the database completely — registration/login routes return 503.
# DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/filemorph

# Stripe (leave empty to disable billing). When present, the /pricing
# page shows live upgrade buttons gated behind the BGB §356 (5)
# withdrawal-waiver checkbox; without these values the page renders a
# "Coming Soon" banner.
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_PRO_PRICE_ID=
STRIPE_BUSINESS_PRICE_ID=

# Whether to expose /pricing as a commercial offer surface at all.
# Self-hosted Community deployments leave this off — there is no
# commercial offer to advertise. Set to `true` only on a SaaS
# deployment that has (or will soon have) Stripe configured.
PRICING_PAGE_ENABLED=false

# S10-lite analytics — per-day counters for page views, conversions,
# registrations, and failures. Visible at /cockpit (admin-only). Counters
# are aggregates, not personal data: no cookie banner needed. Default on;
# set to `false` if you don't want the daily_metrics table populated at
# all (the cockpit Analytics tab then shows an empty-state notice).
METRICS_ENABLED=true
# Transactional email (password-reset, billing receipts, account
# verification). Empty SMTP_HOST disables outgoing mail; the routes
# that need it (forgot-password, register) return a graceful 503.
# SMTP_HOST=smtp.zoho.eu
# SMTP_PORT=587
# SMTP_USER=no-reply@example.com
# SMTP_PASSWORD=
# SMTP_FROM=no-reply@example.com

# ──────────────────────────────────────────────────────────────────────
# === Compliance-Edition tunables (audit log, retention, output integrity) ===
# ──────────────────────────────────────────────────────────────────────
# These knobs target operators in regulated environments (DACH
# Behörden, healthcare, legal). The defaults below are Cloud-edition
# safe (fire-and-forget audit, no retention beyond the request);
# Compliance customers tighten them per their privacy/audit policy.

# NEU-B.1: Compliance-Edition tamper-evident audit log. Default off
# (Cloud-edition fire-and-forget — failures are logged at WARNING and
# never break the request). Set to `true` for ISO 27001 A.12.4.1 /
# BORA §50 / BeurkG §39a compliance — the convert/compress route
# refuses to serve a result it could not log to audit_events. Requires
# DATABASE_URL.
# NEU-B.1Compliance-Edition tamper-evident audit log gate. Default
# off (Cloud-edition: failures are logged at WARNING and never break
# the request). Set to `true` for ISO 27001 A.12.4.1 / BORA §50 /
# BeurkG §39a compliance — the convert/compress route then refuses
# to serve a result it could not log to audit_events. Requires
# DATABASE_URL above.
AUDIT_FAIL_CLOSED=false

# NEU-B.2 retention policy. The Cloud edition is zero-retention by
# design (every conversion flushes its temp dir on completion or
# failure; there is no S3/R2 storage layer active). RETENTION_HOURS
# is an informational knob for self-hosters running a future
# storage-key-backed pipeline (FileJob.expires_at). Compliance-edition
# operators with an eDiscovery / GoBD retention requirement set this
# to the value their privacy policy declares; Cloud / Community keep
# it at 0.
# NEU-B.2 — Retention policy in hours. Cloud edition is zero-retention
# by design (every conversion flushes its temp dir on completion or
# failure; no S3/R2 storage layer is active). This knob is informational
# for self-hosters running a future storage-key-backed pipeline
# (FileJob.expires_at). Compliance-edition operators with an
# eDiscovery / GoBD retention requirement set this to the value their
# privacy policy declares; Cloud / Community keep it at 0.
RETENTION_HOURS=0

# ──────────────────────────────────────────────────────────────────────
# === Operational knobs (sweep cadence, concurrency, metrics) ===
# ──────────────────────────────────────────────────────────────────────

# S10-lite analytics — per-day counters for page views, conversions,
# registrations, and failures. Visible at /cockpit (admin-only).
# Counters are aggregates, not personal data — no cookie banner needed.
# Default on; set to `false` to leave the daily_metrics table empty
# (the cockpit Analytics tab then shows an empty-state notice).
METRICS_ENABLED=true

# Background sweep that removes orphaned `fm_*` temp dirs left behind
# by crashes mid-conversion. The request path always cleans its own
# temp dir in a `finally` block, so this only catches crash-recovery
# cases. Set to 0 to disable the periodic sweep (the startup sweep
# still runs).
# still runs once on boot regardless).
TEMP_SWEEP_INTERVAL_MINUTES=60

# How old (minutes) an `fm_*` temp dir must be before the sweep
Expand All @@ -72,10 +144,10 @@ TEMP_SWEEP_INTERVAL_MINUTES=60
# Operators with very long batch pipelines raise this to match.
TEMP_SWEEP_MAX_AGE_MINUTES=10

# NEU-D.1 capacity guard. Total parallel conversions across all
# callers. Default 4 is sized for a 4 GB host with the existing
# per-tier output caps; raise to ~CPU-count on a bigger box.
# Past the cap → 503 + Retry-After.
# NEU-D.1 — global concurrency cap across all callers. Default 4 is
# sized for a 4 GB host with the existing per-tier output caps; raise
# to roughly CPU-count on a bigger box. Past the cap, requests get
# 503 + Retry-After.
MAX_GLOBAL_CONCURRENCY=4

# How long a request waits for a free slot before giving up. Small
Expand Down
2 changes: 1 addition & 1 deletion .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ FAIL=0
# locale/ catalogs are mechanically extracted from the impressum/privacy/terms templates above —
# they cannot avoid carrying the same address/email strings. Treating them as public is consistent
# with the source templates being public.
ALLOW_RE='^(app/templates/(impressum|privacy|terms)\.html|COMMERCIAL-LICENSE\.md|docs/gdpr-account-deletion-design\.md|docs/api-usage-guide\.md|docs/self-hosting\.md|docs/dpa-template\.md|docs-internal/.*|\.githooks/.*|\.github/workflows/scope-guard\.yml|CHANGELOG\.md|locale/.*\.(po|pot|mo))$'
ALLOW_RE='^(app/templates/(impressum|privacy|terms)\.html|COMMERCIAL-LICENSE\.md|docs/gdpr-account-deletion-design\.md|docs/api-usage-guide\.md|docs/self-hosting\.md|docs/dpa-template\.md|docs-internal/.*|\.githooks/.*|\.github/workflows/scope-guard\.yml|CHANGELOG\.md|locale/.*\.(po|pot|mo)|\.env\.example)$'

# Personal/operational identifiers that should never land in public code.
PATTERNS='lennart\.seidel@icloud\.com|lennart@filemorph\.io|Reetwerder|21029 Hamburg'
Expand Down
2 changes: 1 addition & 1 deletion .githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ set -e
ZERO=0000000000000000000000000000000000000000

# Same patterns as pre-commit — keep in sync.
ALLOW_RE='^(app/templates/(impressum|privacy|terms)\.html|COMMERCIAL-LICENSE\.md|docs/gdpr-account-deletion-design\.md|docs/api-usage-guide\.md|docs/self-hosting\.md|docs/dpa-template\.md|docs-internal/.*|\.githooks/.*|\.github/workflows/scope-guard\.yml|CHANGELOG\.md|locale/.*\.(po|pot|mo))$'
ALLOW_RE='^(app/templates/(impressum|privacy|terms)\.html|COMMERCIAL-LICENSE\.md|docs/gdpr-account-deletion-design\.md|docs/api-usage-guide\.md|docs/self-hosting\.md|docs/dpa-template\.md|docs-internal/.*|\.githooks/.*|\.github/workflows/scope-guard\.yml|CHANGELOG\.md|locale/.*\.(po|pot|mo)|\.env\.example)$'
PATTERNS='lennart\.seidel@icloud\.com|lennart@filemorph\.io|Reetwerder|21029 Hamburg'
OPS_PATTERNS='/opt/filemorph(/|$|[[:space:]])|/var/log/filemorph|/home/deploy([[:space:]]|/)|Hetzner CX|HETZNER_HOST|HETZNER_SSH_USER|HETZNER_SSH_KEY|OPS_REPO_DISPATCH_PAT|GHCR_PAT|appleboy/ssh-action'
SECRET_ASSIGN='(JWT_SECRET|SMTP_PASSWORD|STRIPE_SECRET_KEY|STRIPE_WEBHOOK_SECRET|DATABASE_URL|API_KEY|POSTGRES_PASSWORD|GHCR_PAT|OPS_REPO_DISPATCH_PAT|HETZNER_SSH_KEY)[[:space:]]*=[[:space:]]*[^[:space:]$]'
Expand Down
2 changes: 1 addition & 1 deletion app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Settings(BaseSettings):

api_keys_file: str = "" # resolved below if empty

max_upload_size_mb: int = 2000
max_upload_size_mb: int = 100

cors_origins: str = "http://localhost:8000"
jwt_secret: str = "dev-secret-change-me-min-32-chars-long"
Expand Down
Loading
Loading