Self Host Licensing
This document provides a step-by-step guide to installing and configuring the Bruno Licensing Server on your own infrastructure. The server runs as a Docker container and requires a MongoDB instance for data storage and SMTP credentials for sending email notifications.
Prerequisites
- Docker installed on your server — Install Docker
- Access to a MongoDB instance (on-premise or cloud-hosted, e.g., MongoDB Atlas)
- SMTP credentials from any email provider (SendGrid, Gmail, custom SMTP, etc.)
System Requirements
| Component | Requirement |
|---|---|
| MongoDB Community Server | v6 or greater |
| MongoDB Storage | 4 GB or greater |
| Docker Container Memory | 2 GB or greater |
Architecture Diagram

Setup Methods
The licensing server can be deployed in four ways. Choose the approach that fits your infrastructure:
1. Setup by Running the Docker Image
Pull the image:
docker pull public.ecr.aws/e6d0f8t6/bruno-hosted-license-server
Run the container:
docker run -d \
-e MONGODB_URI="{{ mongodb_uri }}" \
-e SMTP_HOST="{{ smtp_host }}" \
-e SMTP_PORT="{{ smtp_port }}" \
-e SMTP_USER="{{ smtp_user }}" \
-e SMTP_PASSWORD="{{ smtp_password }}" \
-e SMTP_FROM_EMAIL="{{ smtp_from_email }}" \
-e SMTP_FROM_NAME="{{ smtp_from_name }}" \
-e AUTH_JWT_SECRET="{{ random_uuid }}" \
-e BRUNO_GLOBAL_LICENSE_SERVER_URL="https://license-api.usebruno.com" \
-e PORT="80" \
-p 8080:80 \
public.ecr.aws/e6d0f8t6/bruno-hosted-license-server
2. Setup Using Ansible
- hosts: all
become: true
tasks:
- name: Pull Docker image for licensing server
docker_image:
name: public.ecr.aws/e6d0f8t6/bruno-hosted-license-server
source: pull
- name: Run the licensing server container
docker_container:
name: licensing_server
image: public.ecr.aws/e6d0f8t6/bruno-hosted-license-server
state: started
restart_policy: always
ports:
- "8080:8080"
env:
MONGODB_URI: "{{ mongodb_uri }}"
SMTP_HOST: "{{ smtp_host }}"
SMTP_PORT: "{{ smtp_port }}"
SMTP_USER: "{{ smtp_user }}"
SMTP_PASSWORD: "{{ smtp_pass }}"
SMTP_FROM_EMAIL: "{{ smtp_from_email }}"
SMTP_FROM_NAME: "{{ smtp_from_name }}"
AUTH_JWT_SECRET: "{{ random_uuid }}"
BRUNO_GLOBAL_LICENSE_SERVER_URL: "https://license-api.usebruno.com"
PORT: 80
3. Setup Using Docker Compose
docker-compose.yml:
version: '3'
services:
licensing-server:
image: public.ecr.aws/e6d0f8t6/bruno-hosted-license-server
ports:
- "8080:80"
environment:
MONGODB_URI: "your_mongodb_uri"
SMTP_HOST: "your_smtp_host"
SMTP_PORT: "your_smtp_port"
SMTP_USER: "your_smtp_user"
SMTP_PASSWORD: "your_smtp_password"
SMTP_FROM_EMAIL: "your_from_email"
SMTP_FROM_NAME: "your_from_name"
AUTH_JWT_SECRET: "{{ random_uuid }}"
BRUNO_GLOBAL_LICENSE_SERVER_URL: "https://license-api.usebruno.com"
PORT: 80
Start the service:
docker-compose up -d
4. Setup Using Kubernetes
Create the secret:
kubectl create secret generic licensing-server-secrets \
--from-literal=MONGODB_URI="your_mongodb_uri" \
--from-literal=SMTP_HOST="your_smtp_host" \
--from-literal=SMTP_PORT="your_smtp_port" \
--from-literal=SMTP_USER="your_smtp_user" \
--from-literal=SMTP_PASSWORD="your_smtp_password" \
--from-literal=SMTP_FROM_EMAIL="your_from_email" \
--from-literal=SMTP_FROM_NAME="your_from_name" \
--from-literal=BRUNO_GLOBAL_LICENSE_SERVER_URL="https://license-api.usebruno.com" \
--from-literal=AUTH_JWT_SECRET="random_uuid"
licensing-server.yaml — Deployment and Service:
apiVersion: apps/v1
kind: Deployment
metadata:
name: licensing-server
spec:
replicas: 1
selector:
matchLabels:
app: licensing-server
template:
metadata:
labels:
app: licensing-server
spec:
containers:
- name: licensing-server
image: usebruno/hosted-licensing-server
ports:
- containerPort: 8080
env:
- name: PORT
value: "80"
- name: MONGODB_URI
valueFrom:
secretKeyRef:
name: licensing-server-secrets
key: MONGODB_URI
- name: SMTP_HOST
valueFrom:
secretKeyRef:
name: licensing-server-secrets
key: SMTP_HOST
- name: SMTP_PORT
valueFrom:
secretKeyRef:
name: licensing-server-secrets
key: SMTP_PORT
- name: SMTP_USER
valueFrom:
secretKeyRef:
name: licensing-server-secrets
key: SMTP_USER
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: licensing-server-secrets
key: SMTP_PASS
- name: SMTP_FROM_EMAIL
valueFrom:
secretKeyRef:
name: licensing-server-secrets
key: SMTP_FROM_EMAIL
- name: SMTP_FROM_NAME
valueFrom:
secretKeyRef:
name: licensing-server-secrets
key: SMTP_FROM_NAME
- name: BRUNO_GLOBAL_LICENSE_SERVER_URL
valueFrom:
secretKeyRef:
name: licensing-server-secrets
key: BRUNO_GLOBAL_LICENSE_SERVER_URL
- name: AUTH_JWT_SECRET
valueFrom:
secretKeyRef:
name: licensing-server-secrets
key: AUTH_JWT_SECRET
---
apiVersion: v1
kind: Service
metadata:
name: licensing-server
spec:
selector:
app: licensing-server
ports:
- protocol: TCP
port: 8080
targetPort: 8080
Apply the manifest:
kubectl apply -f licensing-server.yaml
Network Ports & Firewall Rules
| System / URL | Port | Description |
|---|---|---|
https://license-api.usebruno.com/ |
443 | Required for the on-premises server to retrieve keys from the Bruno Global Licensing Server. |
| On-Prem License Server | 443 | Ensure port 443 is open on the machine hosting the on-premises server. |
| On-Prem MongoDB Server | 27017 | The on-premises server must be able to communicate with the MongoDB instance. |
Proxy Settings
Pass the following environment variables to configure proxy settings:
| Variable | Value |
|---|---|
PROXY_ENABLED |
true |
HTTP_PROXY |
HTTP proxy URL |
HTTPS_PROXY |
HTTPS proxy URL |
Troubleshooting SSL Issues
The following environment variables disable SSL validation when connecting to the Bruno Global Licensing Server.
| Variable | Value |
|---|---|
BRUNO_GLOBAL_LICENSE_SERVER_DISABLE_SSL |
true |
NODE_TLS_REJECT_UNAUTHORIZED |
1 |
SSL Configuration
The licensing server supports SSL using either a self-signed certificate or a CA-signed certificate.
Supported formats:
- PFX format (.p12)
- CRT + KEY format (.crt and .key)
SSL_KEY_PASSPHRASE is optional and only required if the certificate is encrypted.
Expected file structure:
ssl/
├── certificate.crt
├── certificate.key
└── certificate.p12
Run the container with SSL enabled:
docker run -d \
-e MONGODB_URI="{{ mongodb_uri }}" \
-e SMTP_HOST="{{ smtp_host }}" \
-e SMTP_PORT="{{ smtp_port }}" \
-e SMTP_USER="{{ smtp_user }}" \
-e SMTP_PASSWORD="{{ smtp_password }}" \
-e SMTP_FROM_EMAIL="{{ smtp_from_email }}" \
-e SMTP_FROM_NAME="{{ smtp_from_name }}" \
-e AUTH_JWT_SECRET="{{ random_uuid }}" \
-e BRUNO_GLOBAL_LICENSE_SERVER_URL="https://license-api.usebruno.com" \
-e PORT="80" \
-p 8080:80 \
-v ~/ssl:/opt/license-service/ssl \
-e SSL_KEY_PASSPHRASE="secret" \
public.ecr.aws/e6d0f8t6/bruno-hosted-license-server
Email Configuration
The licensing server supports customizable email templates for license activation, deactivation, and OTP-based login.
Sender Configuration
By default, all emails are sent from Bruno <notifications@usebruno.com>. If your organization enforces email authentication policies (SPF, DKIM, DMARC), you may need to send from a domain you control — otherwise emails may be rejected or flagged as spam.
| Variable | Description | Default |
|---|---|---|
SMTP_FROM_EMAIL |
The "From" email address | notifications@usebruno.com |
SMTP_FROM_NAME |
The display name shown to recipients | Bruno |
Both variables are optional. If omitted, emails will be sent from the default Bruno address.
Template Format
Templates are written in TOML format with two required fields:
| Field | Description |
|---|---|
subject |
Email subject line (supports variables) |
body |
Full HTML body of the email (supports variables) |
Example template:
subject = "Your Bruno License Key - {{orgName}}"
body = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Bruno License Key</title>
</head>
<body>
<h1>Welcome to {{orgName}}!</h1>
<p>Hello {{name}},</p>
<p>Your Bruno license key is ready:</p>
<h2>{{licenseKey}}</h2>
<p>Please keep this license key secure and do not share it with anyone.</p>
<p>If you have any questions, please contact our support team.</p>
<hr>
<p><small>© {{currentYear}} {{orgName}}. Powered by Bruno.</small></p>
</body>
</html>
'''
Available Template Variables
| Variable | Description | Example |
|---|---|---|
{{licenseKey}} |
The generated license key | XXXX-XXXX-XXXX-XXXX |
{{orgName}} |
Organization name | Your Corporation |
{{email}} |
User's email address | user@example.com |
{{name}} |
User's name | John Doe |
{{currentYear}} |
Current year | 2025 |
{{otp}} |
One-Time Password for login (OTP emails only) | 123456 |
Template File Structure
Mount your custom templates using the following structure:
templates/
├── license-activation.toml
├── license-deactivation.toml
└── otp-email.toml
| Purpose | File Name |
|---|---|
| License Activation | license-activation.toml |
| License Deactivation | license-deactivation.toml |
| OTP Login Email | otp-email.toml |
Mount templates when starting the container:
docker run -d \
-e MONGODB_URI="{{ mongodb_uri }}" \
-e SMTP_HOST="{{ smtp_host }}" \
-e SMTP_PORT="{{ smtp_port }}" \
-e SMTP_USER="{{ smtp_user }}" \
-e SMTP_PASSWORD="{{ smtp_password }}" \
-e AUTH_JWT_SECRET="{{ random_uuid }}" \
-e BRUNO_GLOBAL_LICENSE_SERVER_URL="https://license-api.usebruno.com" \
-e PORT="80" \
-p 8080:80 \
-v ~/templates:/opt/license-service/templates \
public.ecr.aws/e6d0f8t6/bruno-hosted-license-server
Template Fallback Behavior
If a custom template is missing or cannot be parsed, the server automatically falls back to the default Bruno template for that notification type. Template usage and fallback events are logged at startup and runtime.
Built-In Template Examples
subject = "Welcome to {{orgName}} - Your Bruno License Key"
body = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bruno License Key</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #d97706; color: white; padding: 10px; text-align: center; border-top-left-radius: 10px; border-top-right-radius: 10px; }
.content { background-color: #f9f9f9; border: 1px solid #ddd; padding: 20px; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; }
.license-key { font-size: 24px; font-weight: bold; margin: 20px 0; letter-spacing: 5px; background-color: #e5e7eb; padding: 15px; border-radius: 5px; text-align: center; }
.footer { text-align: center; margin-top: 20px; font-size: 12px; color: #777; }
</style>
</head>
<body>
<div class="container">
<div class="header"><h1>Welcome to {{orgName}}!</h1></div>
<div class="content">
<p>Hello {{name}},</p>
<p>Your Bruno license key is ready:</p>
<div class="license-key">{{licenseKey}}</div>
<p>Please keep this license key secure and do not share it with anyone.</p>
<p>If you have any questions, please contact our support team.</p>
</div>
<div class="footer"><p><small>© {{currentYear}} {{orgName}}. Powered by Bruno.</small></p></div>
</div>
</body>
</html>
'''
subject = "Bruno License Deactivation Notice"
body = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bruno License Deactivation</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #d97706; color: white; padding: 10px; text-align: center; border-top-left-radius: 10px; border-top-right-radius: 10px; }
.content { background-color: #f9f9f9; border: 1px solid #ddd; padding: 20px; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; }
.footer { text-align: center; margin-top: 20px; font-size: 12px; color: #777; }
</style>
</head>
<body>
<div class="container">
<div class="header"><h1>License Deactivation Notice</h1></div>
<div class="content">
<p>Your Bruno license has been deactivated.</p>
<p>This means you will no longer have access to Bruno's premium features.</p>
<p>If you believe this was a mistake, please contact your License Administrator or software licensing team.</p>
</div>
<div class="footer"><p>© {{currentYear}} Bruno Software Inc. All rights reserved.</p></div>
</div>
</body>
</html>
'''
subject = "Bruno License Manager - Login OTP"
body = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bruno Login OTP</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #d97706; color: white; padding: 10px; text-align: center; border-top-left-radius: 10px; border-top-right-radius: 10px; }
.content { background-color: #f9f9f9; border: 1px solid #ddd; padding: 20px; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; }
.otp { font-size: 24px; font-weight: bold; text-align: center; margin: 20px 0; letter-spacing: 5px; }
.footer { text-align: center; margin-top: 20px; font-size: 12px; color: #777; }
</style>
</head>
<body>
<div class="container">
<div class="header"><h1>Bruno Login OTP</h1></div>
<div class="content">
<p>Your One-Time Password (OTP) for logging into Bruno License Manager is:</p>
<div class="otp">{{otp}}</div>
<p>This OTP is valid for 10 minutes. Please do not share it with anyone.</p>
<p>If you didn't request this OTP, please ignore this email.</p>
</div>
<div class="footer"><p>© {{currentYear}} Bruno Software Inc. All rights reserved.</p></div>
</div>
</body>
</html>
'''
Troubleshooting Template Issues
Template not being used:
- Verify the file exists at /opt/license-service/templates/[template-name].toml
- Check file permissions (must be readable by the container)
- Confirm the server logs show the template was loaded
Template rendering errors:
- Verify valid TOML syntax
- Ensure both subject and body fields are present
- Use Mustache-style variables ({{variable}}), not ${variable}
Common issues:
- Missing or misspelled subject/body fields
- Incorrect variable formatting
- Invalid HTML or character encoding
- Missing read permissions on the template file
SSO for License Admins
The licensing server supports Single Sign-On (SSO) via SAML, allowing License Administrators to log in through your organization's Identity Provider (IdP).
Environment Variables
| Variable | Description | Example |
|---|---|---|
SERVER_URL |
The URL where the server is hosted | https://bruno.internal |
SSO_ENABLED |
Enable or disable SSO | true |
SSO_SAML_SELF_ISSUER_ID |
The SP issuer ID (the server itself acts as the SP) | bruno-license-manager |
SSO_SAML_IDP_CERTIFICATE |
IdP certificate to validate the SAMLResponse signature (from IdP metadata) | MIICnTCCAYUC... |
SSO_SAML_IDP_LOGIN_URL |
The IdP login URL where AuthNRequest is sent | https://idp.internal/realms/testidp/protocol/saml |
SSO_ADMIN_ROLES |
Roles assigned to License Admins in the IdP | bruno-license-admin |
SSO_USER_ROLES |
Roles assigned to Users in the IdP | bruno-license-user |
SSO_SESSION_TIMEOUT_SECONDS |
Session timeout in seconds (default: 3600) |
7200 |
Domain Discovery
Bruno supports domain discovery, which automatically routes users to your organization's license server based on their email domain. To enable this, send us the following details:
- Domain — e.g.,
company.com(as injohn@company.com) - Internal License Server URL — e.g.,
https://bruno.internal
User Workflow for License Activation via SSO
- Open the Bruno app, select the SSO option, and enter your work email.

- You will be redirected to the SSO login page. If
SSO_ENABLED=true, a Login with SSO button will appear.

- After logging in through your IdP, users with the
bruno-license-userrole will be granted access and see the activation confirmation page.

AI Policy
AI Policy lets License Administrators set AI governance for their whole organization from the self-hosted admin portal. Instead of every user configuring AI independently, an admin decides what AI is allowed and how requests are protected, and that policy applies to every user on the server.
The model is simple: the org sets policy, the user brings the key. No API keys are entered or stored in the admin portal. Keys stay user-level, set by each user in the desktop app. A provider or model toggle means allowed or not allowed, not enabled: within the allowed set, users still add their own keys for the providers they want to use.
Accessing AI Policy
- Sign in to your self-hosted admin portal as a License Administrator.
- Select AI Policy from the top navigation bar.
The AI Policy page has two tabs: Configuration (what AI is allowed) and Security (how requests are protected).

Enabling AI features
At the top of the page, the AI Features card is the master switch for your organization. It also summarizes the current policy (for example, 3 providers · 7 models allowed).
- Toggle AI Features on to allow AI usage across your organization.
- Toggle it off to disable all Bruno AI features for every user. When off, the rest of the policy is inert.
Configuration
The Configuration tab controls which providers and models your users can enable.

Providers
Each provider has its own allow/deny toggle and a summary of how many of its models are allowed. Turning a provider off makes it unavailable to users regardless of their own configuration or stored key.
OpenAI: Toggle on to allow it, then expand the row to allow or deny individual models.
Anthropic: Toggle on to allow it, then expand the row to allow or deny individual models.
OpenAI-Compatible Endpoints: Custom endpoints that users configure themselves, for example Azure OpenAI or self-hosted models. Because these are user-defined, there is no per-model control: you can either allow or deny custom endpoints as a whole.
Per-model control
Expand a provider row to allow or deny its individual models. Denying a model removes it from the allowed list. Users cannot select it in the desktop app even when its provider is allowed.
Security
The Security tab governs the data-protection (redaction) rules applied to context Bruno sends to AI providers. These are the same protections available in the Bruno app's Security settings, but set here they can be enforced org-wide.

Let users control security
A Let users control security switch decides who owns the redaction settings:
- On: users manage their own redaction toggles in the desktop app; org policy is ignored.
- Off: the org's redaction policy below is enforced for everyone, and users cannot change it in the app.
Redaction controls
- Redact sensitive header values: mask credential-bearing headers such as
Authorizationand cookies. - Redact sensitive body keys: mask values under keys like
password,*_token, andsecret. - Redact response values: send the response as a shape only, replacing real values with type placeholders.
- Redact secret variable values: mask values of variables that look like secrets.
- Custom redacted headers and custom redacted variables: additional names to always mask.
Saving your policy
Use the buttons at the bottom of the page:
- Save Policy: apply your changes to the whole organization.
- Reset: discard unsaved changes and revert to the last saved policy.
Default state on first deployment
When no policy has been saved, AI is on by default and all providers and models are allowed. Users retain full control of their own security and redaction settings. The lock-down behavior takes effect only once an admin saves a policy.
How the policy reaches users
Policy is delivered to the desktop app through Bruno's existing license verification cycle, which runs when the app opens and every ~24 hours. A policy change can take up to about a day to reach a client that stays open without being restarted.
Enforcement is defense-in-depth: disallowed providers and models are gated in the app's configuration screen and blocked at request time, regardless of any key the user has stored.
What users see in the app
Disabled features are shown in a disabled state with clear messaging. They are not hidden:
- When AI is disabled entirely, the app shows:
AI features are currently disabled. If your access is managed by an administrator, reach out to them to enable it.
- When the Security tab is governed by the org (AI on, settings locked), the app shows:
These security settings are managed for your account and can't be changed here. If your access is managed by an administrator, reach out to them to make changes.
Changelog
Release history for the Bruno Self-Hosted Licensing Server.
1.20.0 (4 August 2026)
- Added AI governance for self-hosted deployments. Admins can set AI policy for the whole organization from the License Manager: turn AI features on or off org-wide, allow or block individual providers and the models within them, and set the data controls applied to AI requests
- AI is on by default, so existing users keep the access they have today. Turning it off is an admin decision
- Policy reaches the desktop app when it opens or at its next daily license verification, so allow up to 24 hours for a change to take effect on an app left running
- Disabled providers and models appear in the app in a disabled state with a message, rather than being hidden
- Changed the Added By value to "-" with an explanatory tooltip, replacing "System", for users added before 1.18.0. Provisioning source has only been recorded since 1.18.0. Users added since then are unaffected, in both the User List and the CSV export
1.19.0 (22 July 2026)
- Added
SMTP_FROM_EMAILandSMTP_FROM_NAMEenvironment variables to allow self-hosted deployments to customise the sender address for SPF/DKIM/DMARC compatibility
1.18.0 (19 June 2026)
- Track user provisioning across the User List, User CSV export, and Audit logs
- Fixed last active value not populating correctly in user CSV export
1.17.0 (30 March 2026)
- Display the app version in the admin dashboard
1.16.2 (2 March 2026)
- Fixed issue where license activation details were not persisted after restarting the app
1.16.1 (19 February 2026)
- Fixed license activation issue
1.16.0 (6 February 2026)
- Added user activation logs
- Added dynamic version logs
- Fixed issue with Docker image updates for default templates
1.15.2 (16 January 2026)
- Fixed issue with Docker image updates
1.15.1 (14 January 2026)
- Fixed session handling for SSO
1.15.0 (9 January 2026)
- Added refresh option for the license information
1.14.0 (1 January 2026)
- Added CSV export support for user data
1.13.0 (4 November 2025)
- Added SCIM support in SSO integration for automated user provisioning and enhanced identity management
1.12.1 (17 October 2025)
- Fixed duplicate license activation page after successful login
1.12.0 (7 August 2025)
- Support for customizing License Activation, Deactivation, and OTP emails
1.11.0 (8 July 2025)
- Support for in-app license activation using SSO
- Users are now notified via email upon deactivation
1.10.1 (10 June 2025)
- Parse Group/Role attributes with list of values (SSO)
1.10.0 (29 May 2025)
- License keys are masked in the UI
- Seat usage is now displayed in the UI
1.9.3 (22 May 2025)
- Retrieve role info from Groups attribute and validate accordingly (SAML, SSO)
1.9.2 (9 May 2025)
- Made Assertion signature non-mandatory (Response signature is required) for SSO
1.9.1 (17 April 2025)
- Fixed issue related to duplicate user during upload
1.9.0 (4 April 2025)
- SSO support for License Admins
1.8.0 (28 March 2025)
- SSL certificate configuration support
1.7.0 (26 November 2024)
- Status API —
/api/v1/status