Documentation

Complete reference for installing, configuring, and operating OpenWhistle — the free, self-hosted whistleblower reporting platform compliant with HinSchG and EU Directive 2019/1937.

Overview

OpenWhistle is a stateless, self-hosted web application that provides a secure internal reporting channel as required by the German Hinweisgeberschutzgesetz (HinSchG) and EU Directive 2019/1937. It is released under the GNU General Public License v3.0.

The application is built with Python and FastAPI, backed by PostgreSQL 18 and Redis 8, and distributed as a Docker image. It has no external dependencies at runtime — no CDN calls, no telemetry, no third-party services.

Key design principles

  • Anonymity first: No IP addresses are stored at any layer. The database schema contains no IP column. Redis session keys carry no identifying metadata.
  • Stateless application: All session state lives in Redis. The application container is horizontally scalable and disposable.
  • Compliance by design: §17 HinSchG deadline tracking is built into the data model, not bolted on as a feature.
  • No vendor lock-in: Standard Docker Compose deployment. Export your data at any time using standard PostgreSQL tools.

Supported platforms

  • Any Linux host with Docker 24+ and Docker Compose v2
  • x86_64 and ARM64 (Apple Silicon) architectures
  • Container registries: GitHub Container Registry (ghcr.io), Docker Hub, Quay.io — images for linux/amd64 and linux/arm64

Current version

v1.0.0 — Envelope encryption at rest, GDPR data retention, multi-tenancy, superadmin role, telephone channel guide. See the CHANGELOG for a full list of changes.

Requirements

OpenWhistle is designed to run on minimal infrastructure. A shared VPS with 1 vCPU and 1 GB RAM is sufficient for most organisations.

Software

  • Docker 24.0 or newer
  • Docker Compose v2.0 or newer (the docker compose plugin, not the legacy docker-compose binary)
  • PostgreSQL 18 and Redis 8 are bundled in the Docker Compose configuration — no separate installation needed

Hardware

  • 512 MB RAM minimum (1 GB recommended)
  • 1 vCPU minimum
  • 5 GB disk space (for application, database, and logs)

Network

  • A domain name pointing to your server
  • A valid HTTPS certificate (Let's Encrypt recommended)
  • Port 80 and 443 open inbound (for nginx)
Important — HTTPS required

Operating a whistleblower reporting system over plain HTTP violates the confidentiality obligations of §8 HinSchG and GDPR. HTTPS is non-negotiable in production deployments.

Container Images

Pre-built multi-arch images (linux/amd64, linux/arm64) are published automatically to three registries on every release and on every commit to main.

Registries

bash
# GitHub Container Registry (primary)
$ docker pull ghcr.io/openwhistle/openwhistle:latest

# Docker Hub
$ docker pull kermit1337/openwhistle:latest

# Quay.io
$ docker pull quay.io/jp1337/openwhistle:latest

Image tags

Tag Updated when Use for
latest Release tag pushed (v*.*.*) Production — always points to the current stable release
1.2.3 / 1.2 / 1 Release tag pushed Pinning to a specific version or minor series
edge Every push to main Testing the latest unreleased development state — not for production
sha-abc1234 Every push (tags and branches) Reproducing an exact build — useful for debugging and rollback
Pinning in production

Replace :latest with a specific version tag (e.g. :0.1.0) in docker-compose.prod.yml for deterministic deployments. The sha- tags are ideal for immutable infrastructure pipelines.

Image signatures

All GHCR images are signed with Cosign (keyless, Sigstore). Verify before pulling in security-sensitive environments:

bash
$ cosign verify ghcr.io/openwhistle/openwhistle:latest \
    --certificate-identity-regexp="https://github.com/openwhistle/OpenWhistle" \
    --certificate-oidc-issuer="https://token.actions.githubusercontent.com"

Installation

The recommended installation method uses Docker Compose. This starts all services (application, PostgreSQL, Redis, nginx) with a single command.

Step 1 — Clone the repository

bash
$ git clone https://github.com/openwhistle/OpenWhistle.git
$ cd OpenWhistle

Step 2 — Configure environment

Copy the example environment file and edit it with your settings. At minimum you must set SECRET_KEY, DATABASE_URL, and REDIS_URL.

bash
$ cp .env.example .env
$ nano .env

# Minimum required values:
SECRET_KEY=your-long-random-secret-key-here
DATABASE_URL=postgresql+asyncpg://openwhistle:password@db:5432/openwhistle
REDIS_URL=redis://redis:6379/0

Step 3 — Start services

bash
$ docker compose up -d
► Network openwhistle_default      Created
► Container openwhistle-db-1        Started
► Container openwhistle-redis-1     Started
► Container openwhistle-app-1       Started
► Container openwhistle-nginx-1     Started

Step 4 — Run the setup wizard

Open http://localhost:4009/setup (or your domain if nginx is configured) in a browser. Follow the wizard to create your first admin account and configure TOTP. The wizard disables itself permanently after completion.

Database migrations

OpenWhistle applies Alembic migrations automatically on startup. The application checks for pending migrations at boot and will not serve traffic until all migrations are applied successfully.

Configuration

All configuration is done via environment variables in the .env file. There are no YAML configuration files or database-stored settings (except admin preferences managed through the UI).

Variable Required Description Default
SECRET_KEY Required Cryptographic key for session signing and CSRF tokens. Generate with openssl rand -hex 32.
DATABASE_URL Required PostgreSQL connection string. Must use the async driver. Format: postgresql+asyncpg://user:password@host:port/dbname
REDIS_URL Required Redis connection string. Format: redis://host:port/db
APP_NAME Optional Display name shown in the application UI and browser title. OpenWhistle
DEMO_MODE Optional Set to true to enable demo mode. See Demo Mode section. false
SECURE_COOKIES Optional Set to false when the app is served over plain HTTP (e.g. local network without TLS). Must be true behind HTTPS. Browsers block Secure cookies over HTTP, causing session failures when accessed from other devices. true
SUBMISSION_MODE_ENABLED Optional Set to false to hide the anonymous/confidential mode selection step from the submission wizard. When disabled, all reports are treated as anonymous. true
OIDC_ENABLED Optional Set to true to enable OIDC login for administrators. false
OIDC_SERVER_METADATA_URL Optional OIDC provider discovery URL (e.g. https://accounts.google.com/.well-known/openid-configuration). Required when OIDC_ENABLED=true.
OIDC_CLIENT_ID Optional OAuth 2.0 client ID from your OIDC provider. Required when OIDC_ENABLED=true.
OIDC_CLIENT_SECRET Optional OAuth 2.0 client secret from your OIDC provider. Required when OIDC_ENABLED=true.
OIDC_REDIRECT_URI Optional OAuth 2.0 callback URL. Must match the redirect URI registered with your OIDC provider. Example: https://yourdomain.com/admin/oidc/callback. Required when OIDC_ENABLED=true.
BRAND_PRIMARY_COLOR Optional Primary brand colour as a hex value. Used for buttons, accents, and links throughout the UI. #0f4c81
BRAND_SECONDARY_COLOR Optional Secondary/accent colour as a hex value. Used for highlights and the submit-page sidebar. #b07230
BRAND_LOGO_URL Optional URL to a company logo image. When set, the logo is shown in the navigation bar instead of the default OpenWhistle shield icon.
APP_PUBLIC_URL Optional Public base URL of the application. Used to build dashboard links inside notification emails and webhooks. Set to your domain (e.g. https://whistleblower.example.com). http://localhost
LOG_LEVEL Optional Logging verbosity. Accepted values: DEBUG, INFO, WARNING, ERROR, CRITICAL. INFO
LOG_FORMAT Optional Log output format. Use json for structured JSON (recommended for log aggregation pipelines) or text for human-readable output. json

Notifications

OpenWhistle can notify administrators via email (SMTP) or a webhook (HTTP POST) whenever a new report is submitted. Both channels are disabled by default and can be configured independently.

Notifications contain only the case number and a timestamp — never the report description or category — to protect the whistleblower's privacy.

Email (SMTP)

VariableRequiredDescriptionDefault
NOTIFY_EMAIL_ENABLED Optional Set to true to send an email notification when a new report arrives. false
NOTIFY_EMAIL_TO Optional Comma-separated list of recipient addresses (e.g. admin@example.com,compliance@example.com).
NOTIFY_EMAIL_FROM Optional Sender (From) address for notification emails. openwhistle@localhost
NOTIFY_SMTP_HOST Optional Hostname of the SMTP server. localhost
NOTIFY_SMTP_PORT Optional SMTP port. Use 587 for STARTTLS or 465 for SMTPS. 587
NOTIFY_SMTP_USER Optional SMTP authentication username. Leave blank for unauthenticated relay.
NOTIFY_SMTP_PASSWORD Optional SMTP authentication password.
NOTIFY_SMTP_TLS Optional Use STARTTLS on the SMTP connection. Set to false when using SMTPS (port 465). true
NOTIFY_SMTP_SSL Optional Use direct TLS (SMTPS, port 465). When true, also set NOTIFY_SMTP_TLS=false. false

Webhook

When enabled, OpenWhistle sends a POST request with a JSON body to the configured URL. If a NOTIFY_WEBHOOK_SECRET is set, the request includes an X-OpenWhistle-Signature: sha256=<hex> header so the receiving endpoint can verify authenticity using HMAC-SHA256.

{
  "event": "new_report",
  "case_number": "OW-2026-00001",
  "timestamp": "2026-04-25T12:00:00+00:00"
}
VariableRequiredDescriptionDefault
NOTIFY_WEBHOOK_ENABLED Optional Set to true to POST a JSON notification to a webhook URL on new reports. false
NOTIFY_WEBHOOK_URL Optional Target URL for webhook POST requests (e.g. a Slack incoming webhook or a custom endpoint).
NOTIFY_WEBHOOK_SECRET Optional HMAC-SHA256 signing secret. When set, each request carries an X-OpenWhistle-Signature header for verification.
NOTIFY_WEBHOOK_TYPE Optional Payload format for webhook notifications. generic sends a plain JSON object; slack sends a Slack Block Kit message; teams sends a Microsoft Teams Adaptive Card (v1.4). generic

SLA Reminders

OpenWhistle can automatically send reminders when HinSchG deadlines are approaching. The background scheduler checks all open reports every 30 minutes. Redis dedup keys prevent duplicate notifications during each warning window.

VariableRequiredDescriptionDefault
REMINDER_ENABLED Optional Set to true to enable automatic SLA reminder notifications. false
REMINDER_ACK_WARN_DAYS Optional Send an acknowledgement reminder this many days before the 7-day deadline expires. 2
REMINDER_FEEDBACK_WARN_DAYS Optional Send a feedback reminder when this many days or fewer remain before the 3-month feedback deadline. 30

Data Retention (GDPR / HinSchG)

Closed reports can be automatically deleted after a configurable retention period. This satisfies GDPR Art. 5(1)(e) (storage limitation) and HinSchG §12 Abs. 3 (3-year minimum documentation requirement). Each deletion writes an immutable audit log entry (report.auto_deleted) recording the case number and legal basis. The admin UI at /admin/retention shows the current configuration and next scheduled run.

VariableRequiredDescriptionDefault
RETENTION_ENABLED Optional Set to true to enable daily automatic deletion of aged closed reports. false
RETENTION_DAYS Optional Days after a report is closed before it is automatically deleted. Do not set below 1095 without legal advice — HinSchG §12 Abs. 3 requires at least 3 years. 1095

Multi-Tenancy

A single OpenWhistle deployment can serve multiple independent organisations (tenants). Each organisation has its own reports, categories, locations, and admin users. The superadmin role manages organisations at /admin/organisations.

VariableRequiredDescriptionDefault
MULTI_TENANCY_ENABLED Optional Set to true to activate multi-organisation support. Requires at least one organisation to exist; the default org is auto-created at setup. false
DEFAULT_ORG_SLUG Optional Slug of the default organisation auto-created during setup. Used for single-tenant deployments and as the fallback for legacy data. default

LDAP / Active Directory Login

Admin accounts can authenticate via corporate LDAP in addition to local credentials and OIDC. On first login, an AdminUser record is auto-provisioned (no local password). TOTP enrollment is still required after the first login.

VariableRequiredDescriptionDefault
LDAP_ENABLED Optional Set to true to allow admin login via LDAP/AD. false
LDAP_SERVER Optional Hostname or IP of the LDAP server (e.g. ldap.example.com).
LDAP_PORT Optional LDAP port. Use 389 for plain/STARTTLS or 636 for LDAPS. 389
LDAP_USE_SSL Optional Set to true to use LDAPS (TLS from the start). Set port to 636. false
LDAP_BIND_DN Optional Distinguished name of the service account used to search the directory (e.g. cn=svc-openwhistle,ou=service,dc=example,dc=com).
LDAP_BIND_PASSWORD Optional Password for the service account bind DN.
LDAP_BASE_DN Optional Search base for user lookups (e.g. ou=users,dc=example,dc=com).
LDAP_USER_FILTER Optional LDAP search filter to locate a user entry. {username} is replaced with the entered username at runtime. (uid={username})
LDAP_ATTR_USERNAME Optional LDAP attribute to use as the username in the provisioned admin record. uid
LDAP_ATTR_EMAIL Optional LDAP attribute to read the user's email address from. mail

S3-Compatible Attachment Storage

By default, file attachments are stored as binary data in PostgreSQL (STORAGE_BACKEND=db). For large-scale deployments, set STORAGE_BACKEND=s3 to store new attachments in an S3-compatible bucket. Existing DB-backed attachments are not migrated and continue to work.

VariableRequiredDescriptionDefault
STORAGE_BACKEND Optional Storage backend for new attachments. db stores data in PostgreSQL; s3 stores data in an S3-compatible bucket. db
S3_ENDPOINT_URL Optional Custom endpoint URL for S3-compatible services (e.g. MinIO, Hetzner Object Storage). Leave blank for AWS S3.
S3_BUCKET_NAME Optional Name of the target S3 bucket. Required when STORAGE_BACKEND=s3.
S3_ACCESS_KEY_ID Optional AWS access key ID (or MinIO equivalent). Required when STORAGE_BACKEND=s3.
S3_SECRET_ACCESS_KEY Optional AWS secret access key (or MinIO equivalent). Required when STORAGE_BACKEND=s3.
S3_REGION Optional AWS region for the bucket (e.g. eu-central-1). Ignored for custom endpoints. us-east-1
S3_PREFIX Optional Key prefix for all stored objects (e.g. attachments/). Useful when sharing a bucket with other applications. attachments/

File Attachments

Whistleblowers can attach evidence files when submitting a report. Files are stored as binary data in PostgreSQL alongside the report — no additional storage infrastructure is required.

Limits and allowed types

  • Maximum file size: 10 MB per attachment
  • Maximum number of attachments: 5 per report
  • Allowed formats: PDF, JPEG, PNG, GIF, WebP, TXT, CSV, DOCX (.docx / .doc), XLSX (.xlsx / .xls)

File type is validated by both MIME type and file extension. SVG, executable files, archives, and all other types are rejected.

Access control

  • Whistleblower: downloads are available at /status/attachments/{id} and require an active ow-status-session cookie. The session must be tied to the same report that owns the attachment — a whistleblower cannot access another report's files.
  • Admin: downloads are available at /admin/reports/{report_id}/attachments/{id} and require an active admin session. The attachment must belong to the specified report.

All downloads use Content-Disposition: attachment to prevent the browser from rendering files inline. This eliminates MIME-sniffing and script-injection risks.

Privacy and deletion

Attachments are stored in the attachments database table with a CASCADE DELETE foreign key constraint. When a report is hard-deleted (DSGVO Art. 17 right to erasure), all associated attachments are automatically and permanently removed.

First-Run Wizard

On first boot, the /setup route serves a multi-step browser-based wizard. The wizard is automatically disabled once an admin account exists in the database.

Wizard steps

  1. Create admin account
    Enter a username (3–64 characters) and a strong password (minimum 12 characters).
  2. Configure TOTP
    Scan the displayed QR code with an authenticator app (Google Authenticator, Authy, Bitwarden, etc.). Enter the 6-digit code to confirm the secret is correctly registered. TOTP is mandatory and cannot be skipped.
Save your TOTP secret

Write down the TOTP secret key displayed during setup and store it securely offline. There are no backup codes. If you lose access to your authenticator app, you will need to reset the TOTP secret directly in the database.

Admin Guide

The admin portal is accessible at /admin. Login requires your username, password, and a current TOTP code (or OIDC if configured).

Dashboard overview

The dashboard shows all reports with sortable columns, pagination, and filters for status, assignee, and location. Each row displays:

  • Unique case number in OW-YYYY-NNNNN format (sequential per year)
  • Submission date and time
  • Report category
  • Assignee (admin or case manager)
  • §17 acknowledgement deadline countdown (red when under 24 hours)
  • §17 feedback deadline countdown
  • Unread message indicator

Roles

Every admin account has one of three roles:

  • Superadmin — all admin capabilities plus organisation management (/admin/organisations). Required for multi-tenant deployments. Superadmin accounts have org_id = NULL (cross-organisation scope).
  • Admin — full access including user management, categories, locations, audit log, and report deletion.
  • Case Manager — can view, comment on, and advance reports assigned to them; cannot manage users or delete reports.

Manage admin accounts at /admin/users. The last active admin account cannot be deactivated to prevent lockout.

Managing reports

Click any report to open the case view. From here you can:

  • Read the full report content and submission mode (anonymous / confidential)
  • Advance the case status through the workflow: received → in_review → pending_feedback → closed
  • Assign the case to a specific admin or case manager
  • Send a message to the whistleblower via the anonymous channel
  • Add internal notes (never shown to the whistleblower)
  • Link related cases together
  • Export the full case as a PDF (includes SLA compliance section per HinSchG §17)
  • Request hard deletion — a second admin must confirm (4-eyes principle); triggers GDPR Art. 17 erasure

Audit log

Every admin action is recorded in an immutable audit log (required by HinSchG §12 Abs. 3). View and filter the log at /admin/audit-log; export as CSV at any time.

Categories and locations

Report categories are database-driven and fully configurable at /admin/categories. Multiple branches or office locations can be managed at /admin/locations; when active locations exist, the whistleblower wizard shows a location selector at step 2.

Dashboard statistics

/admin/stats shows the total report count, status distribution bar chart, category breakdown, and the 7-day SLA compliance rate.

HinSchG deadline tracking

The dashboard enforces the statutory timelines from §17 HinSchG:

  • 7-day acknowledgement: The countdown starts from report submission. The dashboard shows a yellow warning at 3 days, red at 24 hours.
  • 3-month feedback: The countdown starts from the date of acknowledgement (§17 Abs. 2). A feedback response must be sent via the message channel before the deadline.

Telephone reporting channel guide

The page at /admin/telephone-channel provides a compliance reference for operating an oral reporting channel alongside the digital channel. It covers:

  • HinSchG §16 requirements (dedicated number, confidentiality, impartial staff)
  • Implementation options: internal hotline vs. external ombudsman
  • §10 HinSchG recording prohibition — calls may not be recorded without consent
  • Integration workflow: how telephone reports enter the OpenWhistle case system
  • Legal references with links to the official statute text

Whistleblower Guide

The whistleblower submission form is accessible at the root URL of your OpenWhistle installation (e.g. https://your-domain.example.com/). No account, email address, or personal information is required.

Submitting a report

Submission uses a guided multi-step wizard. You can navigate back and forward at any step; your partial progress is saved in a temporary session (valid for 2 hours).

  1. Navigate to the submission URL. Use a private browsing window and, where possible, a network that is not traceable to you (public Wi-Fi, Tor, etc.) for maximum protection.
  2. Choose submission mode. Select Anonymous (no personal data ever stored) or Confidential (name, contact details, and optional secure email stored encrypted — only decryptable by the assigned admin). Both modes are fully compliant with HinSchG.
  3. Select location (if the operator has configured multiple branches or offices). This step only appears when locations are active.
  4. Choose a category from the operator's configured list (e.g. financial fraud, safety violation, discrimination).
  5. Write the description. This is the only mandatory field. Be as specific as possible without naming yourself.
  6. Attach evidence (optional). Supported formats: PDF, images, Word, Excel, CSV, TXT — up to 10 MB each, 5 files per report.
  7. Review and submit. Immediately note down the case number and PIN displayed on the confirmation screen. These are shown only once and cannot be recovered if lost.

Checking report status

Navigate to /status on the same installation. Enter your case number and PIN. You will see:

  • Current case status
  • Admin responses (if any)
  • HinSchG deadlines: the 7-day acknowledgement deadline and 3-month feedback deadline with days remaining
  • A form to send a follow-up message

Security recommendations for whistleblowers

  • Do not submit from a device or network that can be traced to you
  • Use a browser in private/incognito mode
  • Store the case number and PIN in a secure location not connected to your work identity
  • Never share the PIN with anyone

Security Architecture

OpenWhistle is designed with a defence-in-depth approach to anonymity. There are four independent layers that prevent IP addresses from reaching persistent storage.

The four anonymity layers

nginx — Strip X-Forwarded-For
The nginx reverse proxy configuration explicitly strips the X-Forwarded-For, X-Real-IP, and all X-Client-* headers before forwarding requests to the application. Even if a CDN or upstream proxy adds these headers, they are removed at the nginx layer.
Application middleware — Drop remote address
A FastAPI middleware intercepts every request before it reaches any route handler and replaces the client.host with 0.0.0.0. No route handler can access the real remote address even if it tried.
Database schema — No IP column
The reports, messages, and sessions tables have no ip_address column. It is structurally impossible for the ORM to persist an IP address — not just a matter of policy.
Redis sessions — No identifying metadata
Session tokens in Redis are UUID4 values. The session data structure contains only the case number (not the PIN) and an expiry timestamp. No IP, no User-Agent, no browser fingerprint is stored in session data.

Rate limiting without IP tracking

Brute-force protection on the report access endpoint uses Redis-based rate limiting keyed on the session token, not the IP address. A fixed-window counter increments on each failed PIN attempt. After 5 failed attempts within 15 minutes, the session token is invalidated and a new one must be obtained. The whistleblower's IP address is never used as a rate-limiting key.

Security headers

The nginx configuration applies a strict set of HTTP security headers to all responses:

  • Strict-Transport-Security with max-age=31536000; includeSubDomains
  • Content-Security-Policy restricting scripts, styles, and frames to self
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: no-referrer
  • Permissions-Policy disabling geolocation, camera, microphone

OIDC integration

When OIDC is configured, admin login can be completed entirely via the external provider. The application validates the authorization code, fetches the userinfo endpoint, extracts the sub claim, and maps it to a local admin account. A session is created directly — no separate TOTP step is required for OIDC logins. The admin account must be pre-linked to an OIDC identity by a system administrator.

Demo Mode

Demo mode is intended for the public demo instance at demo.openwhistle.net. It should not be used for production deployments.

Enabling demo mode

.env
DEMO_MODE=true

Demo mode behaviour

  • Prefilled admin credentials: username demo, password demo, TOTP code 000000
  • The static TOTP code 000000 is always accepted — no authenticator app needed
  • All reports and messages are visible to the demo admin
  • A banner is displayed on all pages indicating this is a demo instance
  • The demo instance at demo.openwhistle.net is automatically wiped and restarted every hour via a cron job
Never submit real reports to the demo

The demo instance is public and accessible to anyone. Demo data is wiped hourly but may be readable by other users in the interim. Never submit real or sensitive information to the demo instance.

Upgrading

OpenWhistle follows semantic versioning. Minor and patch releases are backward-compatible. Major releases may include breaking changes documented in the CHANGELOG.

Standard upgrade procedure

bash
# Pull the latest image
$ docker compose pull

# Restart containers with the new image
$ docker compose up -d

# Database migrations are applied automatically on startup
$ docker compose logs app | grep migration

Before upgrading

  • Read the CHANGELOG for the target version
  • Back up your PostgreSQL database: docker compose exec db pg_dump -U openwhistle openwhistle > backup.sql
  • Note your current version with docker compose exec app python -m app --version

Rollback

To roll back to a specific version, pin the image tag in docker-compose.yml and re-run docker compose up -d. Database migrations cannot be automatically rolled back — restore from backup if a migration causes issues.

Stay up to date

Watch the GitHub repository for new releases. Security fixes are released as patch versions. Enable GitHub's Dependabot or watch the repository for release notifications.