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 composeplugin, not the legacydocker-composebinary) - 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)
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
# 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 |
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:
$ 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
$ 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.
$ 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
$ 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.
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)
| Variable | Required | Description | Default |
|---|---|---|---|
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"
}
| Variable | Required | Description | Default |
|---|---|---|---|
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.
| Variable | Required | Description | Default |
|---|---|---|---|
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.
| Variable | Required | Description | Default |
|---|---|---|---|
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.
| Variable | Required | Description | Default |
|---|---|---|---|
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.
| Variable | Required | Description | Default |
|---|---|---|---|
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.
| Variable | Required | Description | Default |
|---|---|---|---|
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 activeow-status-sessioncookie. 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
-
Create admin account
Enter a username (3–64 characters) and a strong password (minimum 12 characters). -
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.
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-NNNNNformat (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 haveorg_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).
-
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.
-
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.
-
Select location (if the operator has configured multiple branches or offices). This step only appears when locations are active.
-
Choose a category from the operator's configured list (e.g. financial fraud, safety violation, discrimination).
-
Write the description. This is the only mandatory field. Be as specific as possible without naming yourself.
-
Attach evidence (optional). Supported formats: PDF, images, Word, Excel, CSV, TXT — up to 10 MB each, 5 files per report.
-
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
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.
client.host with
0.0.0.0. No route handler can access the real remote address
even if it tried.
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.
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-Securitywithmax-age=31536000; includeSubDomainsContent-Security-Policyrestricting scripts, styles, and frames to selfX-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: no-referrerPermissions-Policydisabling 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
DEMO_MODE=true
Demo mode behaviour
- Prefilled admin credentials: username
demo, passworddemo, TOTP code000000 - The static TOTP code
000000is 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.netis automatically wiped and restarted every hour via a cron job
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
# 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.
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.