This is Beacon, a lightweight consent and opt-out service built with .NET 10. Beacon manages email consent state independently of any ERP, CRM, or automation platform. It issues secure, temporary URLs for opt-out and preference changes, validates them without upstream dependencies, and exposes a simple API that other systems query before sending email.
Feel free to play around in our live demo instance, available on beacon-demo.melosso.com. Use
Beacon-Api-Keyas your access token.
Beacon is a centralized consent and communication-preference service. It allows external systems (ERP, CRM, marketing tools, automation platforms) to store, check, and update email permission states through a single, consistent API.
Consent is organized into logical groupings called Buckets (for example: newsletters, campaigns, or customer programs). For every email address in a bucket, Beacon can generate a secure, temporary URL that lets recipients manage their own preferences. These URLs can be embedded directly into outgoing messages and validated independently using cryptographic signatures.
Beacon is designed to be:
- Decoupled: no business logic in your sending systems
- Stateless where possible: token validation without upstream lookups
- Extensible: usable as a standalone service or embedded into existing flows
Beyond basic opt-out handling, Beacon also supports richer consent workflows such as signup forms, multiple permission states, and administrative management via its built-in web interface.
Noteworthy features include:
- Token-based opt-out: Secure HMAC-signed URLs that validate without database lookups (unless you want to)
- Multi-database support: SQLite (default), SQL Server, PostgreSQL, MySQL
- Admin panel: Web UI for managing buckets and viewing consent records
- Confirmation mails: You can choose for (double) opt-in confirmations via e-mail notifications
- Form builder: Create campaign and newsletter signup forms
- Granular permissions: Set multiple permission states in a single API call
- Security first: Encrypted data at rest, hashed emails, rate limiting
In other words, Beacon provides a decoupled infrastructure for managing communication preferences, allowing you to externalize consent logic and opt-out processing from your primary data sources.
We've prepared two methods to deploy Beacon. It's up to you to choose your preferred method:
services:
beacon:
image: ghcr.io/melosso/beacon:latest
ports:
- "5000:5000" # Public API
- "5001:5001" # Admin panel
volumes:
- beacon_data:/app/data # Database storage
- beacon_core:/app/.core # Encryption keys
environment:
# Core settings (required)
- Beacon__SigningKey=${BEACON_SIGNING_KEY}
- Beacon__EncryptionKey=${BEACON_ENCRYPTION_KEY}
- Beacon__Pepper=${BEACON_PEPPER}
- Beacon__AdminApiKey=${BEACON_ADMIN_API_KEY}
- Beacon__ConnectionString=Data Source=/app/data/Beacon.db
# Port-based routing (default, no reverse proxy)
- Beacon__ApiPort=5000
- Beacon__AdminPort=5001
# Host-based routing (for reverse proxy deployments)
# - Beacon__ApiHosts=beacon-api.example.com
# - Beacon__AdminHosts=beacon-admin.example.com
# - Beacon__AllowedOrigins=https://app.example.com
# - Beacon__TrustForwardedHeaders=true
volumes:
beacon_data:
beacon_core:# Create the .env file
[ -f .env ] && echo ".env already exists! Aborting." && exit 1; ADMIN_KEY=$(openssl rand -base64 48 | tr -d '\n'); ENC_KEY=$(openssl rand -base64 32); printf "BEACON_SIGNING_KEY=%s\nBEACON_ENCRYPTION_KEY=%s\nBEACON_PEPPER=%s\nBEACON_ADMIN_API_KEY=%s\n" "$(openssl rand -base64 32)" "$ENC_KEY" "$(openssl rand -base64 32)" "$ADMIN_KEY" > .env && echo "Your X-Api-Key is: $ADMIN_KEY"
# Start the container
docker compose up -dAccess the Admin panel at http://localhost:5001 and API at http://localhost:5000.
Download the latest release from Releases.
- Install .NET 10 Runtime:
winget install --id Microsoft.DotNet.Runtime.10 -e- Set encryption key:
$bytes = New-Object byte[] 48; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes); [Environment]::SetEnvironmentVariable("BEACON_ENCRYPTION_KEY", [Convert]::ToBase64String($bytes), "Machine")- Install service:
.\Beacon.bat install
.\Beacon.bat start- Open browser → http://localhost:5000 / http://localhost:5001
On first run, sensitive configuration values in appsettings.json will be automatically encrypted. You should, ofcourse, safely store your API key to keep access to the admin panel too.
For production deployments with host-based routing, see the Configuration section.
Beacon will provide you a simple, non-customizable API that does one thing: securely store permissions for an e-mail address in a bucket. Your application can use this API to create a new permission state in the bucket–and return a token. This JWT-token contains all data, allowing the user to access its data without putting load on the database:
https://beacon.acme-corporation.com/u/v1.eyJiIjoicTEtY2FtcGFpZ24iLCJlIjoia...You can incorporate this in your newsletters, system notifications, or anything you'd like – allowing your user to configure their permissions in decentralized system and keeping them outside of your data source:
As Beacon is an API-first platform, all consent management operations should be handled programmatically. While manual execution via the web UII is possible, integration typically involves automating these calls within your specific workflow. The first step requires creating a permission state for an email address in a bucket–which triggers the automatic creation of the target bucket if it is not already present.
Creates consent records and returns a signed opt-out token ([{"token":"<signed_jwt>"}]).
curl -X POST http://localhost:5000/api/tokens/generate \
-H "X-Api-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '[{
"bucket": "q1-campaign",
"email": "user@example.com",
"permissions": {
"newsletter": true,
"marketing": false
}
}]'Response is an array of [{"token":"<signed_token>","doubleOptIn":false}]. Access the first element for single-item requests.
If you're planning on updating the permission record after insertion, may want to use configure skipPermissionUpdate to prevent overwriting (user) updated permissions.
User clicks the token link to update preferences.
GET /u/{token}
Query consent status before sending email.
curl -X POST http://localhost:5000/api/consent/check \
-H "X-Api-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"bucket": "q1-campaign", "email": "user@example.com", "permission": "newsletter"}'curl -X POST http://localhost:5000/api/consent/override \
-H "X-Api-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"bucket": "q1-campaign", "email": "user@example.com", "permission": "newsletter", "status": "OptedIn"}'curl -X DELETE http://localhost:5000/api/admin/buckets/q1-campaign \
-H "X-Api-Key: your-api-key"# Supported languages: en (default), de, fr, nl, pl, es
curl -X POST http://localhost:5000/api/tokens/generate \
-H "X-Api-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '[{
"bucket": "q1-campaign",
"email": "beige@example.com",
"permissions": {
"newsletter": true,
"marketing": false
},
"customFields": {
"externalId": "external-reference"
}
"allowReplay": false,
"expiryDays": 30,
"language": "nl",
"skipPermissionUpdate": true
}]'Depending on your environment, these settings are changed in your .env, docker-compose.yml or appsettings.json file.
| Variable | Purpose | Default |
|---|---|---|
Beacon__DatabaseProvider |
sqlite, sqlserver, postgres, mysql | sqlite |
Beacon__ConnectionString |
Database connection string | Data Source=Beacon.db |
Beacon__SigningKey |
HMAC signing key (base64, 32 bytes) | Required |
Beacon__EncryptionKey |
AES-256 encryption key (base64, 32 bytes) | Required |
Beacon__Pepper |
Email hashing pepper | Required |
Beacon__AdminApiKey |
API key for authenticated endpoints | Required |
Beacon__TokenExpiryDays |
Default token validity period | 30 |
When deploying behind a reverse proxy (nginx, Traefik, Caddy), use host-based routing to separate public API and admin traffic on different subdomains:
| Variable | Purpose | Example |
|---|---|---|
Beacon__ApiHosts |
Hosts for public API access | beacon-api.example.com |
Beacon__AdminHosts |
Hosts for admin panel access | beacon-admin.example.com |
Beacon__AllowedOrigins |
Additional CORS origins | https://app.example.com |
Beacon__TrustForwardedHeaders |
Trust X-Forwarded-* headers from proxy | true |
Beacon__PublicUrl |
Override the base URL used in confirmation email links | https://beacon-api.example.com |
When using double opt-in emails, Beacon builds confirmation links using the first entry of
Beacon__ApiHosts(prefixed withhttps://).Beacon__PublicUrlis only needed when the public URL cannot be derived fromApiHosts. So, for example, in port-based deployments without a configured hostname, or when the external URL differs from the API host (CDN, custom domain).
When ApiHosts/AdminHosts are not configured, Beacon uses the following ports that can be overridden by changing the following variables:
| Variable | Purpose | Default |
|---|---|---|
Beacon__ApiPort |
Port for public API endpoints | 5000 |
Beacon__AdminPort |
Port for admin panel and OpenAPI docs | 5001 |
You may need to combine both the host- and port-based variables when working with a reverse proxy (e.g. Cloudflare Tunnels or Pangolin).
You can configure the following additional, completely optional settings:
| Variable | Purpose | Default |
|---|---|---|
Beacon__UserAuthentication |
Allow user authentication, API tokens, or both (user, api, both). Leave blank to only allow Beacon__AdminApiKey |
both |
# Linux/macOS
openssl rand -base64 32
# PowerShell
$b = New-Object Byte[] 32; [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); [Convert]::ToBase64String($b)Free for open source projects and personal use under the AGPL 3.0 license. For more information, please see the license file.
Contributions are always welcome! Please submit issues and pull requests, using the templates we provided.