88 KiB
⬡ Haven: User Guide
Welcome to Haven, your private chat server. This guide covers everything you need to get Haven running and invite your friends.
What You Need
- Windows 10 or 11 (macOS / Linux can run it manually)
- Node.js version 22 to 26 → Download here
- About 50 MB of disk space
- OR just Docker, no Node.js needed
Docker Setup (Alternative)
If you'd rather run Haven in a container (great for NAS boxes, servers, or if you just like Docker):
Quick Start
Option A: Pre-built image (fastest):
docker pull ghcr.io/ancsemi/haven:latest
docker run -d -p 3000:3000 -v haven_data:/data ghcr.io/ancsemi/haven:latest
Option B: Build from source:
git clone https://github.com/ancsemi/Haven.git
cd Haven
docker compose up -d
That's it. Haven will be running at https://localhost:3000.
What Happens Automatically
- Self-signed SSL certs are generated on first launch (needed for voice chat)
- Database, config, and uploads are stored in a Docker volume (
haven_data) - The container runs as a non-root user for security
- Restarts automatically if it crashes
Customizing
Edit docker-compose.yml to change the port, server name, or other settings. The environment variables are commented out with examples. Just uncomment what you need.
Using a Local Folder Instead of a Volume
If you want your data in a specific folder (common on Synology / NAS):
volumes:
- /path/to/your/haven-data:/data
Replace the haven_data:/data line in docker-compose.yml.
Podman (Fedora and similar)
Haven runs under Podman as well. A few things differ from Docker:
- SELinux. On Fedora, add
:Zto a folder mount so the container may use it:-v ./haven-data:/data:Z. - Ports below 1024. Rootless Podman cannot listen on 443. Publish Haven's own port (
-p 3000:3000) and, if you want people outside to reach it on 443, forward it in the firewall:sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=3000 sudo firewall-cmd --reload - File owners. Inside the container Haven runs as uid 1000, and the container hands the data folder to that user each time it starts. If you copy data in from another machine, or run Haven once outside the container against the same folder, and the container cannot fix the owner itself, it says so in the log. Fix it from the host with:
If you start the container with
podman unshare chown -R 1000:1000 ./haven-data--userns=keep-id, uid 1000 in the container is your own account (when your account is uid 1000), so usesudo chown -R $(id -u):$(id -g) ./haven-datainstead. - Updating. The container runs the copy of Haven built into the image, not files on your machine, so extracting a new zip or running Haven outside the container does not update it (and running it outside against the data folder changes the file owners). Pull the new image and recreate the container; the data folder stays as it is:
Then run the same
podman pull ghcr.io/ancsemi/haven:latest podman rm -f havenpodman runcommand you started it with.
Updating
Option A: Pre-built image (default, recommended):
docker compose pull
docker compose up -d --force-recreate
Option B: Built from source (only if you uncommented build: .):
git pull
docker compose build --no-cache
docker compose up -d
Your data is safe: it lives in the volume, not the container.
Early Access (Beta) Builds
New features reach the :beta image a few days before :latest. To run them, change the image line in docker-compose.yml to:
image: ghcr.io/ancsemi/haven:beta
then pull and recreate as above. With docker run or Podman, pull ghcr.io/ancsemi/haven:beta and start the container from that image instead. Your data stays as it is. To leave early access, set the line back to :latest once the next stable release is out, so you never move to an older version than the one you ran.
Updating Automatically
The Update Now button in Settings can't update a Docker install: a container can't replace itself, and giving Haven control of Docker would give it control of the whole machine. To stay current without logging in, let the host do it on a schedule. On Linux (a Raspberry Pi included), run crontab -e and add a line like this one, with your own folder:
0 4 * * * cd /home/pi/haven && docker compose pull -q && docker compose up -d
That checks for a new image every night at 4:00 and restarts Haven only when there is one. It follows whichever image docker-compose.yml names, :latest or :beta.
Checking Your Version
Open this URL in your browser (replace with your domain/IP if needed):
https://localhost:3000/api/version
Or from inside the container:
docker compose exec haven cat /app/package.json | grep '"version"'
Linux Prerequisites
If you're on Linux (Ubuntu, Mint, Debian, etc.), make sure you have Docker's official packages installed. The default docker.io package from some distros may be missing Compose V2.
1. Install Docker Engine + Compose plugin:
sudo apt update
sudo apt install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$UBUNTU_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin
2. Add your user to the docker group (so you don't need sudo for every command):
sudo usermod -aG docker $USER
newgrp docker
After that, docker compose up -d should work without errors.
Proxmox VE (LXC)
If you run Proxmox VE in your homelab, an automated unprivileged LXC helper script is available. It supports automatic Caddy reverse proxy with Let's Encrypt SSL, Tailscale mesh VPN, or direct LAN modes.
See the Proxmox VE LXC Deployment Guide for quick-start instructions and configuration flags.
Getting Started
Step 1: First Launch
Double-click Start Haven.bat
That's it. The batch file will:
- Check that Node.js is installed
- Install dependencies (first time only)
- Generate SSL certificates (first time only)
- Start the server
- Open your browser to the login page
Step 2: Create Your Admin Account
- On the login page, click Register
- Create an account with the admin username (default:
admin; check your data directory's.envfile) - This account can create and delete channels
Step 3: Create a Channel
- In the sidebar, use the Create Channel box (admin only)
- Give it a name like "General" or "Gaming"
- Haven generates a unique channel code (8 characters)
- Share this code with your friends: it's the only way in
Step 4: Invite Friends
Send your friends:
- Your server address:
https://YOUR_IP:3000 - The channel code
They'll register their own account, then enter the code to join your channel.
Channels & Sub-Channels
How Channels Work
Every conversation in Haven happens inside a channel. Channels are like rooms: each has a unique 8-character code (e.g. a3f8b2c1). To get into a channel, you either create it or enter its code.
Forum Channels 🗂️
Turn any channel into a forum from Channel Functions → Forum, or tick Forum when creating it. In a forum channel every message is a topic, and the feed runs newest first: the most recently active topic sits at the top. Replies go in that message's thread, the topic shows its reply count, and a new reply moves the topic back to the top, so old topics resurface instead of sinking. Inside a topic's thread, replies read top to bottom as usual. Every topic carries a Reply to this topic button, and the Reply action on a topic opens its thread rather than quoting it as a new topic. Everything else works as usual: topics can be pinned, reacted to, searched, and moved between channels with Move Messages.
Creating Sub-Channels
Right-click (or click ⋯) on any channel to create a sub-channel beneath it. Sub-channels appear indented under their parent with a ↳ icon. They have their own code and their own message history.
When you create a sub-channel:
- All current parent channel members are automatically added to it
- The sub-channel gets its own unique invite code
- Max one level deep (no sub-sub-channels)
When someone joins a parent channel later:
- They're automatically added to all non-private sub-channels of that parent
- They do NOT get access to private sub-channels (see below)
Private Sub-Channels 🔒
When creating a sub-channel, check the 🔒 Private checkbox. Private sub-channels:
- Only add the creator as initial member (not all parent members)
- Show a 🔒 icon instead of
↳in the sidebar - Appear in italic text with reduced opacity
- Can only be joined by entering the sub-channel's code directly
- Are invisible to non-members (they won't see it in their channel list)
Use private sub-channels for admin-only discussions, sensitive topics, or small breakout groups within a larger channel.
Any channel can be switched between private and public later from Channel Functions → Private. Going private keeps everyone who is already a member. Going public opens the channel to the whole server, history included.
Importing from Discord
Haven can copy a Discord server's message history into Haven channels. The easiest way uses your Ferry bot: nothing to download, nothing to paste.
Method 1: Your Ferry bot (recommended)
- Set up Ferry if you haven't yet. The Ferry section walks you through it and takes about two minutes. For importing you only need its first two parts: create the bot, then connect it to Haven and add it to your Discord server. You don't have to turn Ferry on or pair any channels.
- In Haven, open Settings (⚙️ in the sidebar) → Import Discord History, then click the 🔗 Connect to Discord tab
- Click Use my Ferry bot
- Pick your Discord server, then tick the channels and threads you want
- Click Fetch Messages and wait while Haven copies them. A big server can take a few minutes
- In the preview, rename channels if you want, then click Import Selected
Discord keeps private channels hidden from the bot until its role is added to them. Haven greys those channels out with a 🔒 so you can see which ones they are. To bring one in, add the bot's role to that channel in Discord with View Channel and Read Message History (here's how), then pick your server again. If a channel still can't be read, Haven skips it and tells you which ones it skipped.
Don't give the bot Administrator to get around this. Anyone who got hold of its token would then control your whole Discord server.
What gets imported: messages, replies, embeds, reactions, pins, links to attachments, and the original Discord names and avatars.
Channel types supported: text, announcement, forum, media, plus active and archived public threads.
Method 2: Upload an export file
If you'd rather not set up a bot, Discord can give you a download of your own messages (only yours, not everyone's):
- In Discord, go to User Settings → Data & Privacy → Request all of my Data. Discord emails you a ZIP when it's ready, which can take a few days
- In Haven, open Settings → Import Discord History. The 📁 Upload File tab is already open
- Drop the ZIP in, or click browse to find it
- In the preview, rename channels if you want, then click Import Selected
Export files from DiscordChatExporter (JSON) work here too.
Important Notes
- Imported messages appear as the original Discord usernames, but they're all stored under the admin account. They're clearly marked as imported from Discord.
- The import is history only: Discord roles, permissions, bots, and webhooks are not imported.
- To keep chatting with people who stay on Discord, use Ferry to bridge channels between the two.
Join Code Settings (Admin)
Each channel's invite code can be configured by admins. Click the ⚙️ gear icon next to the channel code in the header.
Code Visibility
| Setting | Behavior |
|---|---|
| Public | All members can see the channel code |
| Private | Only admins see the code; others see •••••••• |
Code Mode
| Setting | Behavior |
|---|---|
| Static | Code never changes |
| Dynamic | Code automatically rotates based on a trigger |
Rotation Triggers (Dynamic mode only)
| Trigger | Behavior |
|---|---|
| Time-based | Code rotates every X minutes |
| Join-based | Code rotates after X new members join |
You can also click Rotate Now to manually change the code immediately.
💡 Dynamic codes are great for public communities where you want to limit code sharing. Old codes stop working after rotation.
Location Data in Photos and Videos
Phones save where a photo or video was taken inside the file itself. Haven removes that when someone uploads to a channel, so nobody can work out where it was taken.
- Every server: JPEG, PNG and WebP photos are cleaned. There's nothing to install.
- With exiftool and ffmpeg installed: videos and every other photo format (HEIC, RAW and so on) are cleaned too. The Docker image already has both.
- Linux:
sudo apt install ffmpeg libimage-exiftool-perl(or your distro's equivalent), then restart Haven. - Windows:
winget install Gyan.FFmpeg, and download exiftool from exiftool.org. Renameexiftool(-k).exetoexiftool.exeand either put it on your PATH or setHAVEN_EXIFTOOL_PATHto it, then restart Haven.
- Linux:
- What it can't reach: files in encrypted DMs (they're encrypted before the server ever sees them), copies people already downloaded, files Ferry already sent to Discord, and your backups.
The first start after updating also goes through files uploaded before this existed. With the tools installed, a file that can't be cleaned is held back instead of shown, and its message says so. The server log says at startup which of the two modes it's in.
Avatars
Uploading a Profile Picture
- Click the ⚙️ Settings button in the sidebar
- In the Avatar section, click Upload
- Choose an image (max 2 MB; JPEG, PNG, GIF, or WebP)
- Pick a shape: ⚪ Circle, ⬜ Square, ⬡ Hexagon, or ◇ Diamond
- Click Save
Your avatar and shape are visible to everyone in messages and the member list. Each user's shape is stored independently.
Removing Your Avatar
Click Clear to remove your avatar and revert to the default initial-letter avatar.
Themes & Effects
Themes
Haven includes 20+ visual themes. Click the 🎨 button at the bottom of the sidebar to open the theme picker. Every theme has a small preview card in its own colors; type in the search box to find one by name, author or description, and fold away a section you do not need. Community themes the admin has published come first, then Seasonal ones, then Haven's own. Themes change colors, fonts, and overall aesthetic. Your choice is saved to your account.
| Button | Theme | Style |
|---|---|---|
| ⬡ | Haven | Deep blue/purple (default) |
| 🎮 | Discord | Dark gray with blue accents |
| Ⅿ | Matrix | Black and green, scanline overlay |
| ◈ | Tron | Black with neon cyan glow |
| ⌁ | HALO | Military green with Mjolnir vibes |
| ⚜ | LoTR | Parchment gold and deep brown |
| 🌆 | Cyberpunk | Neon pink and electric yellow |
| ❄ | Nord | Arctic blue and frost |
| 🧛 | Dracula | Deep purple and blood red |
| ⚔ | Bloodborne | Gothic crimson and ash |
| ⬚ | Ice | Pale blue and white |
| 🌊 | Abyss | Deep ocean darkness |
Servers also include the optional Compact Theme API v1 file theme. An admin can publish it from Settings → Admin → Branding → Custom Themes. Compact tightens the desktop chrome while preserving the user's separate message-density choice under Settings → Layout.
Effect Overlays
Effects are stackable visual layers on top of any theme. Choose from the effect selector in the theme popup:
| Effect | Description |
|---|---|
| ⟳ Auto | Matches your current theme's default effect |
| 🚫 None | No overlays |
| 📺 CRT | Retro scanlines + vignette + flicker |
| Ⅿ Matrix | Green digital rain cascade |
| ❄ Snowfall | Falling snowflakes |
| 🔥 Campfire | Ember particles + warm glow |
| 💍 Golden Grace | Elden Ring-style golden particles |
| 🩸 Blood Vignette | Dark pulsing edges |
| ☢️ Phosphor | Fallout-style green vignette |
| ⚔️ Water Flow | Gentle blue sidebar animation |
| 🧊 Frost | Ice shimmer + icicle borders |
| ⚡ Glitch | Cyberpunk text scramble (see below) |
| ⚜ Candlelight | Warm sidebar glow |
| 🌊 Ocean Depth | Deep blue vignette |
| ✝️ / ⛪ / 🕊️ | Sacred themed overlays |
Cyberpunk Text Scramble ⚡
When the Glitch effect is active, text around the UI randomly "scrambles", cycling through random characters before resolving back to the original text. This affects:
- The HAVEN logo
- Channel names in the sidebar
- Section labels
- Your username
- The channel header
- User names in the member list
A Glitch Frequency slider appears in the theme popup when this effect is active. Slide left for rare, subtle glitches, or right for constant chaos.
Bundled optional themes and plugins
Haven also ships optional extras that are installed but switched off by default, so you will not see them until an admin turns them on. They are already on your server, including in the Docker image. There is nothing to download.
| File | What it is |
|---|---|
themes/compact.theme.css |
Compact, a dense graphite Theme API v1 theme |
themes/braid.theme.css |
Braid, a dark mint theme |
themes/braid-light.theme.css |
Braid Light |
themes/amni-scient.theme.css |
Amni-Scient, gold on ink with the Archivo typeface |
themes/amni-scient-light.theme.css |
Amni-Scient Light |
themes/glassy-neon.theme.css |
Glassy Neon, Haven on the Grid: holo-glass panels with lit edge rails, light-cycle trails, circuit traces, a racing perspective light floor and a spinning identity disc |
themes/christmas.theme.css |
Christmas, a cozy cabin with a crackling fireplace, a twinkling tree and presents, string lights, frosted window edges and three layers of falling snow |
themes/halloween.theme.css |
Halloween, a cartoony haunted night with bats over the moon, a floating ghost, flickering jack-o'-lanterns, a black cat, a bubbling cauldron, fog and dripping goo |
themes/thanksgiving.theme.css |
Thanksgiving, a harvest evening with tumbling leaves, swaying trees, wobbling turkeys, hay bales, corn and pumpkins |
plugins/CompactLayout.plugin.js |
Reversible desktop layout that pairs with Compact or any other theme |
plugins/ChromeLayout.plugin.js |
Condensed sidebar with docked DMs, prominent + action, and channel voice controls |
plugins/BraidLayout.plugin.js |
Braid's layout changes |
plugins/ChannelTabsLayout.plugin.js |
Channels and DMs tabs above the sidebar list, with a Join or create button |
plugins/MessageTimestamps.plugin.js |
Adds timestamps to messages |
plugins/HavenGlyphs.plugin.js |
Reversible contextual interface icons using the bundled local Font Awesome font |
public/fonts/fa-solid-900.woff2 |
Local Font Awesome Solid font used by Haven Glyphs |
To make a bundled theme available to everyone, publish it. The quickest way is the theme picker itself: admins (and anyone with Manage Server) see an Available to publish section there with every theme nobody has published yet, and each theme file's card has a Publish switch. Settings → Admin → 🏠 Branding → Custom Themes does the same. Until a theme is published, members do not see it in their picker or in Settings → Plugins & Themes and cannot switch to it, even though the file is present, which is the usual reason a theme "looks missing" after an update. Someone who was already using a theme when it was unpublished keeps it until they pick another.
Custom themes work the same way. Drop a <name>.theme.css file into the themes/ folder, restart, then publish it in the same place. A theme can also be set as the server default from that section, which applies to anyone who has not already picked a theme of their own. It is a default rather than a lock, so a user who chooses a different theme keeps their choice.
Administrators can check supported plugins and themes for updates under Settings → Admin → Extension Updates. Extension authors can add update support using the metadata and GitHub release format in the extension update and maintainer guide.
Enable Compact Layout per browser under Settings → Plugins & Themes. It
folds the server rail into the navigation sidebar and docks account and active
voice controls in that sidebar's footer on desktop. Tablet and mobile keep the
native Haven layout. Use its C footer button or Ctrl+Alt+C to switch between
Compact and classic layout without disabling the plugin.
Only one structural layout plugin can be active at a time. If Braid Layout is
already engaged, Compact waits until Braid restores the native layout, and vice
versa. The Sidebar Layout picker in Settings → Layout switches between them: Original
(Haven's own layout), then every layout plugin on the server. A new layout
plugin appears there by itself; see Appearing in the Layout picker.
Haven Glyphs is optional per browser under Settings → Plugins & Themes. It uses the bundled local Font Awesome font for interface affordances. Message bodies, reactions, emoji/GIF pickers, soundboard names, user content, and form fields stay unchanged. It can be disabled from the same settings screen.
Theme authors who need stable CSS variables and semantic layout selectors should use the Theme API v1 authoring reference instead of depending on Haven's internal classes and IDs.
If a theme or plugin makes the interface unusable, open /app.html?haven-safe-mode=1. Haven will suppress file themes and avoid fetching or evaluating plugins for that tab, allowing the extension choices to be reset from Settings → Plugins & Themes.
Background images (wallpapers)
A theme is ordinary CSS, so it can set a background image and not just colours.
Put the image in themes/ next to your .theme.css file, then point at it with
a plain relative path. Haven serves the whole themes/ folder, so
url("wallpaper.jpg") resolves to /themes/wallpaper.jpg on its own.
.main {
background-image:
linear-gradient(rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.55)),
url("wallpaper.jpg");
background-size: cover;
background-position: center;
}
.main is the chat area. The message list, the channel header and the messages
themselves have no background of their own, so the image shows through behind
all of them.
The linear-gradient on top is a dark scrim, and it is doing real work: over a
bright or busy photo, message text becomes unreadable without it. Raise 0.55
towards 1 to dim the image further, lower it to let more through, or delete
that line entirely if your image is already dark. On a light theme, use white
(rgba(255, 255, 255, 0.6)) instead.
The sidebar keeps its own colour and is not covered. Add .sidebar to the same
rule if you want the image there too, or set it on body to cover everything.
Two things worth knowing before you plan around this:
- A theme is a file on the server, so adding one needs access to the server itself, and publishing it puts it in the theme picker for everyone. There is no per-user wallpaper setting. It is personal only in the sense that a theme applies to whoever picks it.
- Keep the file reasonably small. Every member downloads it when they select the theme, and a 12 MB photo is a 12 MB download.
Docker users:
themes/andplugins/live inside the image, not in the/datavolume, so pulling a newer image is all you need to get new bundled files. Remember to recreate the container afterwards (docker compose up -d), since pulling alone leaves the old container running the old image. If you bind-mount over/app, your host folder wins and you will need to add the files there yourself.
Setting Up Remote Access (Friends Over the Internet)
If your friends are not on your local WiFi, you need a way for them to reach your machine. There are three routes here, least safe first:
- Port forwarding (this section): simplest, but it puts your Haven login page in front of the whole internet.
- Tailscale / WireGuard: the safest. Nothing is exposed publicly and you never touch your router.
- Cloudflare Tunnel: a public URL without opening a port or revealing your home IP.
Haven can also run LocalTunnel or Cloudflared for you from Settings, Admin, Tunnel if you would rather not install anything by hand.
Before you port forward, know what it costs
Opening a port means anybody on the internet can reach your Haven login page. Some of that is not obvious up front:
- Bot networks. Automated scanners sweep the whole internet looking for misconfigured services. Within minutes of forwarding a port, your address starts getting probed. Almost all of it bounces off Haven and your firewall, but the risk is not zero, and it uses a little of your bandwidth around the clock.
- DHCP drift. Most home routers hand out addresses dynamically, so the machine running Haven can land on a different local IP after a reboot. Your port forward then points at whatever device took the old address, which means your friends cannot reach Haven and something else on your network is exposed instead. Assign the Haven machine a static IP to prevent this, see the DHCP reservation step below.
- Password guessing. Haven rate-limits sign-in attempts (20 per 15 minutes per IP address) and supports two-factor authentication, so it is not defenceless. But a login page on the open internet will be tried, so use a password you have not used anywhere else and turn on MFA.
If that is acceptable to you, carry on. If not, use Tailscale or a tunnel instead.
Find Your Public IP
Visit whatismyip.com. The number shown (like 203.0.113.50) is what your friends will use.
Port Forwarding on Your Router
Every router is different, but the general steps are:
-
Log into your router: usually
http://192.168.1.1orhttp://10.0.0.1in your browser -
Find Port Forwarding (sometimes called NAT, Virtual Servers, or Applications)
-
Create a new rule:
Field Value Port 3000Protocol TCP Internal IP Your PC's local IP (e.g. 10.0.0.60) -
Save and apply
How to find your local IP: Open Command Prompt and type
ipconfig. Look for the "IPv4 Address" under your Ethernet or WiFi adapter.
Windows Firewall
The server needs permission to accept incoming connections:
- Open Start Menu → search "Windows Defender Firewall"
- Click "Advanced settings" on the left
- Click "Inbound Rules" → "New Rule..."
- Select Port → TCP → enter
3000 - Allow the connection → apply to all profiles
- Name it something like "Haven Chat"
Or run this in PowerShell (as Administrator):
New-NetFirewallRule -DisplayName "Haven_Chat" -Direction Inbound -LocalPort 3000 -Protocol TCP -Action Allow
Tell Your Friends
Send them this URL:
https://YOUR_PUBLIC_IP:3000
⚠️ Certificate Warning: Your friends' browsers will show a security warning because Haven uses a self-signed certificate. This is normal and expected. Tell them to click "Advanced" → "Proceed to site". The connection is still encrypted.
No OpenSSL? No problem. If there is no certificate when Haven starts, it makes one itself, so HTTPS works on a clean Windows install. To run plain HTTP on purpose (behind a reverse proxy that handles TLS, or LAN only), set
FORCE_HTTP=truein your.env. Voice, camera and the mobile app need HTTPS on any address other than localhost.
Tailscale / WireGuard (No Port Forwarding, No Exposed IP)
Port forwarding above opens your Haven login page to the entire internet. Tailscale does not: it builds an encrypted tunnel straight between your machine and your friend's, so nothing is exposed publicly and you never touch your router or firewall. It is the safest of the three methods here.
Voice chat over a shared device
Text chat, uploads, and everything that flows through the Haven server work perfectly with the shared-device setup below. Voice is the exception. Haven voice is peer-to-peer: everyone in a call opens a direct connection to every other person in the call, not just to the host. Sharing a single device only puts the host machine on the shared path, so your friends can reach the host but not each other, and the return path from the host back to a friend isn't guaranteed either. The classic symptom is one-directional audio: everyone hears the host, but the host can't hear anyone. This isn't a bug in the shared-device config, it's that a single shared machine can't provide the full mesh of paths a peer-to-peer call needs.
Two ways to get voice working, most recommended first:
- Add a TURN server under Settings, Admin, Voice & Connectivity (STUN/TURN). TURN relays all voice media through one reachable point, so the peer-to-peer paths are no longer required and the secure single-device sharing below keeps working exactly as written. You can run your own coturn or use a hosted TURN provider.
- Put everyone on the same tailnet instead of sharing one device. Tailnet members get direct connectivity to one another, so the mesh forms and voice works both ways. This works, but it gives your friends broader access than device-sharing does, so read Step 4 below and only do this if you accept that tradeoff.
-
Unlike port forwarding, Tailscale does not require you to touch your router or your computer's firewall. Tailscale uses Wireguard under the hood, which is the same protocol many reputable VPN companies use. Wireguard creates an encrypted, end-to-end tunnel from your computer to your friend's computer. Most firewalls, like the one your router and computer use, allow outbound connections by default. Tailscale establishes a persistent, outbound connection to the Tailscale coordination server, which then allows your friends to connect. This is why configuring your firewall is not a requirement for this method.
-
Anyone who wants access to your Haven server will first need a Share link generated by you, the administrator, via your Tailscale admin dashboard. If you follow this guide correctly, your friend will only have access to Haven and strictly nothing else on your device.
-
The obvious tradeoff with this approach is setup. Both you and your friends will need to connect to Tailscale anytime you want to access Haven. But the bright side of this approach is once setup is complete, and you're logged in, connecting to Tailscale is as simple as flipping a switch. Tailscale works on pretty much all devices you can think of, and it's seamless.
-
To be clear, Tailscale does change your device's DNS settings. But unlike a traditional VPN, Tailscale does not route your internet traffic through some remote server. Tailscale is purely a connection between your computer, and your friend's computer. Your IP does not change and your internet speed doesn't slow down in any meaningful way.
Step 1 - Make an account
-
Visit https://tailscale.com and make an account. Tailscale will walk you through downloading Tailscale onto your device, you may proceed with that.
-
Once Tailscale is running on your machine, it may ask you to add another device, click the "Skip" button at the bottom of the page.
Step 2: Navigate to Tailscale admin page
Click here to access the admin page. Here, you will see a list of all the devices on your Tailnet.
Step 3: Lock down access
- Currently, if you share access to your Tailscale device, any other ports that are open on your machine will be reachable by your friends. We solve this issue by setting up ACL rules. ACL Rules will ensure only Haven is accessible by your friends, and strictly nothing else.
- On your Tailscale admin page, click "Access Controls".
- Click "JSON Editor" and replace everything in that section with the following, secure config:
- Please ensure you copy everything, including the trailing comma at the end of the code block
{ "grants": [ // autogroup:member are members of your tailnet. We are sharing a device with your friends, NOT adding them to our tailnet. So you are the only person on your tailnet who should have this permission. And therefore, we are giving members of this tailnet unrestricted access to everything. { "src": ["autogroup:member"], "dst": ["*"], "ip": ["*"], }, // autogroup:shared are your friends who are connecting to your shared machine. This control restricts the port your friends may use to connect to your machine. If you changed your Haven port, make sure to change the ports to whatever port you set. Otherwise, leave everything below as default. { "src": ["autogroup:shared"], "dst": ["*"], "ip": ["3000", "3001"], }, ], }
Step 4: Share, not invite
- This step is critical. There is a fundamental difference between inviting someone to your tailnet, and simply sharing one machine. The settings above DO NOT apply if you invite someone to your tailnet, and they will get access to your whole tailnet. If your Haven computer is running any other web server, or if you add more Tailscale devices down the road, your friends will have access to them. You do not want this.
- To SHARE a device, go to your Tailscale admin page, click the 3 dot menu next to your device, and click Share. If you are sharing a link, make sure Reusable link is turned off. This way, each link you use only works once, and you maintain complete control over who can access your shared device.
Step 5: Sharing the link
- Once you provide a share link to your friend, he will need to make an account on Tailscale, download the client and connect on his machine. Please note, your friend does not need to share anything from his end. Only the person hosting Haven will have to share.
- Once your friend accepts the link, his device will now be able to reach your shared device.
- Sharing is one way. Your friend's device will not appear in your admin page, and it does not need to. If he can see your machine on his end, the share worked.
Step 6: Usage
- Once everything is wired up, go to your Tailscale admin page, find your device, and notice the IP Address listed next to your device. This is your Tailnet IP address. It is not your actual IP address.
- Accessing Haven is as simple as going to https://TailscaleIPAddress:3000 (or whatever port you configured in ACL settings)
- Please note: If you are the one hosting Haven, you may also access Haven from your LAN IP that Haven is running on (typically 192.168.X.X), but your friends need to use your Tailscale IP.
Cloudflare Tunnel (No Port Forwarding)
If you don't want to mess with port forwarding or expose your home IP, you can use a Cloudflare Tunnel to securely share your Haven server over the internet. Cloudflare gives your server a public URL and handles all the networking: no router config needed.
Step 1: Install Cloudflared
Windows (via winget):
winget install cloudflare.cloudflared
macOS (via Homebrew):
brew install cloudflared
Linux:
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared
Verify it installed:
cloudflared --version
Step 2: Enable the Tunnel in Haven
- Start Haven normally (
Start Haven.bat) - Log in as admin
- Open ⚙️ Settings → Admin → Tunnel
- Pick Cloudflared in the Provider list
- Click Start Tunnel
- Haven will start cloudflared and display your public URL (e.g.
https://abc-def-123.trycloudflare.com)
Step 3: Share the URL
Copy the tunnel URL and send it to your friends. That's it: no port forwarding, no firewall rules, no IP address sharing. The URL changes each time you restart the tunnel, so you'll need to re-share it.
How It Works
- Haven runs cloudflared as a child process that creates an encrypted tunnel to Cloudflare's network
- Cloudflare assigns a random public URL and proxies traffic through the tunnel to your local server
- Your home IP is never exposed to visitors; they only see Cloudflare's IP
- Since Haven runs HTTPS with a self-signed cert, the tunnel connects to
https://localhost:3000with TLS verification disabled (the Cloudflare→You leg is already encrypted by the tunnel itself)
Tunnel vs. Port Forwarding
| Port Forwarding | Cloudflare Tunnel | |
|---|---|---|
| Router config | Required | None |
| Exposes home IP | Yes | No |
| Firewall rules | Required | None |
| Stable URL | Your IP (may change) | Random URL (changes on restart) |
| Push notifications | ✅ (if HTTPS) | ✅ |
| Voice chat | ✅ | ✅ |
💡 Tip: For a permanent URL, you can set up a free Cloudflare account and use a named tunnel with your own domain. See Cloudflare's tunnel docs for details.
Troubleshooting Tunnels
| Problem | Solution |
|---|---|
| "cloudflared not found" | Restart your terminal after installing, or add it to your PATH manually |
| Tunnel shows "502 Bad Gateway" | Make sure Haven is running before enabling the tunnel |
| URL changes every restart | Normal for quick tunnels. Use a named tunnel + custom domain for permanence |
| "Connection refused" in tunnel logs | Haven isn't running on port 3000, or it's running HTTP instead of HTTPS |
Reverse Proxy (Caddy, nginx, Traefik)
If you already have a domain and want Haven to live behind a proper reverse proxy (so you get a real Let's Encrypt cert, no browser warnings, and the same https://chat.example.com URL every time), set FORCE_HTTP=true and let the proxy terminate TLS for you.
Quick Recipe (Caddy)
-
Stop Haven if it's running.
-
Add the following line to your
.envfile (create one next topackage.jsonif it doesn't exist):FORCE_HTTP=trueThis tells Haven to skip its built-in self-signed cert generation and listen on plain HTTP on port 3000. Caddy will handle the HTTPS leg.
-
Install Caddy (caddyserver.com/download) and create a
Caddyfile:chat.example.com { reverse_proxy localhost:3000 }Replace
chat.example.comwith your real domain. Caddy will auto-fetch a Let's Encrypt cert on first run. Make sure ports 80 and 443 are open / forwarded to the Caddy host. -
Start Caddy, then start Haven (
Start Haven.batornpm start). -
Open
https://chat.example.comin a browser. You should see Haven with a clean padlock and no cert warnings.
Using a Tunnel + Caddy
If you don't want to port-forward 80/443, point a tunnel (Cloudflare Tunnel, Tailscale Funnel, ngrok, etc.) at the Caddy host. The flow becomes:
Browser → Tunnel (HTTPS) → Caddy (HTTPS) → Haven (HTTP, FORCE_HTTP=true)
Caddy still terminates TLS for the LAN leg, and the tunnel terminates a second TLS layer for the public leg. That's the setup minecraft_bread used successfully (see the support thread for the full step-by-step).
nginx Snippet
server {
listen 443 ssl http2;
server_name chat.example.com;
ssl_certificate /etc/letsencrypt/live/chat.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/chat.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
}
The Upgrade / Connection headers are required for Socket.io WebSocket traffic; without them voice chat and live messages will silently break.
Common Gotchas
| Problem | Fix |
|---|---|
| Browser shows Haven's self-signed cert warning instead of the Let's Encrypt one | You forgot FORCE_HTTP=true. Haven is still serving its own HTTPS on 3000 and Caddy is just proxying that. Add the line, restart Haven. |
| Voice chat / live updates don't work behind nginx | Add the Upgrade and Connection "upgrade" headers shown above. Caddy handles WebSockets automatically. |
| Mixed-content errors in browser console | Make sure the proxy forwards X-Forwarded-Proto $scheme so Haven knows it's serving over HTTPS. |
| "502 Bad Gateway" from Caddy | Haven isn't running, or it's still bound to HTTPS on 3000. Double-check FORCE_HTTP=true is in .env and you restarted Haven after adding it. |
💡 A full Docker Compose example with Traefik + coturn lives in
docs/examples/haven-traefik-coturn/if you'd rather run the whole stack containerised.
Outgoing Proxy
If the server can only reach the internet through a proxy (for example a Docker container with no route out and a filtering proxy beside it), set the standard proxy variables in .env:
https_proxy=http://proxy.example:3128
http_proxy=http://proxy.example:3128
no_proxy=localhost,127.0.0.1
Link previews, the media proxy, bot callbacks, push notifications and Haven's other outgoing requests then go through the proxy. Hosts listed in no_proxy (names, subdomains, IP addresses or CIDR ranges) connect directly. Only http:// proxy addresses are supported. Voice traffic is UDP and does not use the proxy.
For a link a member posts (link previews, the media proxy, bot callbacks, push), Haven looks the name up itself, refuses private, loopback and cloud metadata addresses, and then sends the proxy to the address it checked: a CONNECT to that IP for https:// sites, and the IP in the request line (with the site's name in the Host header) for http:// sites. The proxy never has to look the name up again, so it can't be steered to a different address.
A name the server can't resolve on its own is refused. If your server has no DNS of its own and only the proxy can resolve names, set:
OUTBOUND_PROXY_RESOLVES_NAMES=true
⚠️ With that setting, your proxy becomes part of Haven's security. Names Haven can't resolve are then passed to the proxy unchecked, and the proxy decides where they go. A proxy that can reach your internal network could let members use link previews to reach hosts behind it. Restrict the proxy to the destinations Haven needs, ideally with whole-hostname allowlist rules.
External Media Storage (S3)
By default, attachments, avatars, borders, emoji, stickers, sounds, badge icons, role icons and the server icon and banner live in the uploads folder of Haven's data folder. You can keep them in an S3-compatible bucket instead: Garage, MinIO, RustFS, SeaweedFS or AWS S3. The database, certificates and .env stay in the data folder either way.
HAVEN_EXTERNAL_STORAGE=true
HAVEN_S3_ENDPOINT=http://garage:3900
HAVEN_S3_BUCKET=haven-media
HAVEN_S3_REGION=garage # AWS: your region, such as us-east-1
HAVEN_S3_ACCESS_KEY=...
HAVEN_S3_SECRET_KEY=...
# HAVEN_S3_PREFIX=haven/ # optional: keep Haven's files under a prefix
# HAVEN_S3_FORCE_PATH_STYLE=true # the default; Garage, MinIO and RustFS need it
- It's an add-on, downloaded only if you turn it on. Haven talks to the bucket through Amazon's S3 library, which isn't part of a normal install or the Docker image. The first start with
HAVEN_EXTERNAL_STORAGE=trueinstalls it into the data folder (addons/s3, about 16 MB, at versions pinned by Haven), so that start needs to reach registry.npmjs.org once. A bucket on your own NAS still needs no outside service after that. - The bucket stays private. Haven reads each file and serves it itself, with the same safety headers as files on disk, so don't make the bucket public.
- Haven won't start without it. If the bucket can't be reached or the key is wrong, Haven stops at startup and names the endpoint, the bucket and the reason. It doesn't fall back to the disk.
- Location data is still removed first. Uploads are cleaned on the server before they reach the bucket, and photos already in the bucket are checked once.
Moving an existing server to a bucket
- Set the variables above, plus
HAVEN_S3_LOCAL_FALLBACK=true, and restart. Files still on disk keep being served while you move them. - Copy them over. It's safe to run again, and nothing on disk is removed yet:
node scripts/migrate-uploads-to-s3.js # Docker: docker compose exec haven node scripts/migrate-uploads-to-s3.js - Remove
HAVEN_S3_LOCAL_FALLBACKand restart. Everything should still load. - Free the disk space. This only removes local files once the bucket holds all of them:
node scripts/migrate-uploads-to-s3.js --delete-local
If the server finds files on disk that aren't being served, it says so at startup.
Backups
The bucket is the backup of the files themselves; use your storage's own replication or snapshots for it. Haven's backups still include the files when you choose files, copied out of the bucket one at a time, which takes a while on a large server. With scheduled backups, every run copies the whole bucket. Restoring a backup puts its files back into the bucket on the next start.
If files are added to or removed from the bucket outside Haven, rebuild Haven's list of them with node scripts/reconcile-media.js.
Router-Specific Tips
Xfinity / Comcast (XB7 Gateway)
- Open the Xfinity app on your phone
- Go to WiFi → scroll down → Advanced settings → Port forwarding
- Select your PC from the device list
- Add port
3000(TCP/UDP) and apply - Important: Go to Home → disable xFi Advanced Security (it silently blocks all inbound connections)
- Verify the reserved IP in port forwarding matches your PC's actual IP (
ipconfigto check)
Common Issues
| Problem | Solution |
|---|---|
| "SSL_ERROR_RX_RECORD_TOO_LONG" | Browser is using https:// but server is running HTTP. Change URL to http://localhost:3000, or unset FORCE_HTTP and restart (see Troubleshooting below) |
| Friends get "took too long to respond" | Port forwarding not set up, or firewall blocking |
| Friends get "connection refused" | Server isn't running: launch Start Haven.bat |
Can't connect with https:// |
Make sure you're using port 3000, not 443 |
| Voice chat doesn't work | Use https:// (or http://localhost on the server itself). Browsers only allow the microphone on a secure connection |
| "Certificate error" in browser | Normal. Click Advanced → Proceed |
Voice Chat
- Join a text channel first
- Click 🎤 Join Voice in the channel header
- Allow microphone access when your browser asks
- Click 🔇 Mute to toggle your mic
- Click 📞 Leave to disconnect from voice
Voice chat is peer-to-peer by default: audio goes directly between you and other users, not through the server. Big servers can turn on the optional voice relay instead (Settings, Large Server Setup).
Voice needs a secure connection. On the server itself,
http://localhost:3000orhttps://localhost:3000both work. From any other device, usehttps://YOUR_IP:3000.
TURN Server (Voice Over the Internet)
By default, voice/screen sharing uses STUN servers, which work when both users are on the same network or behind simple NATs. For connections across different networks (especially mobile data / 5G), you need a TURN server to relay traffic.
Quick setup with coturn (free, open-source):
# Ubuntu/Debian
sudo apt install coturn
# /etc/turnserver.conf:
listening-port=3478
tls-listening-port=5349
realm=your-domain.com
use-auth-secret
static-auth-secret=YOUR_RANDOM_SECRET_HERE
# Only if coturn is behind a router (home server, NAT'd VM, Docker bridge).
# Leave this out when the machine holds its public IP directly.
external-ip=YOUR_PUBLIC_IP
Then add to your Haven .env:
TURN_URL=turn:your-server.com:3478
TURN_SECRET=YOUR_RANDOM_SECRET_HERE
Restart Haven, and voice/screen sharing will work across any network.
You can set this in the app instead: Settings → Admin → 📡 Voice & Connectivity configures the same STUN and TURN servers without touching
.env, and it applies without restarting.
Which one wins: a TURN server set in Settings takes precedence, and the
TURN_*environment variables are ignored while it's filled in. The env vars only apply when the in-app TURN field is empty. If you setTURN_URLin.envand nothing seems to change, open Settings and clear the field there.TURN_SECRET(time-limited HMAC credentials) is environment-only, and is skipped entirely once a TURN server is configured in the app.
Docker users: Add
TURN_URLandTURN_SECRETas environment variables in yourdocker-compose.yml. See the commented example in the default compose file.
Oracle Cloud / cloud VMs: Make sure ports 3478 (UDP+TCP) and 49152 to 65535 (UDP) are open in your security group / firewall rules. These are needed for TURN relay traffic.
Home server behind a router: this is where TURN most often looks configured but silently does nothing, because coturn has no idea it is behind NAT and hands out its LAN address as the relay. Three things to get right:
- Set
external-ip=YOUR_PUBLIC_IPinturnserver.conf. Without it every relay candidate points at a private address that nobody outside your network can reach. It wants a literal IP, not a hostname, so if yours is dynamic you have to update the line and restart coturn when it changes.- Forward UDP, not just TCP. TURN media is UDP: port 3478 plus the whole
min-porttomax-portrange. A TCP-only forward lets coturn start up, answer, and relay nothing.- Turn on NAT reflection (also called hairpin NAT) on your router, or people on your own LAN cannot reach
turn:your-domain.com, because that name resolves to your public IP. pfSense and OPNsense call it NAT Reflection. Some routers do not support it at all, in which case split DNS pointing the name at the LAN address is the way round it.The quickest check is inside Haven: Settings, Voice & Connectivity, Test voice connectivity. It runs your STUN and TURN the same way a call does and tells you which one is not answering and why. Bear in mind it runs from your browser, so a server that only answers on your own network passes there and still fails for everyone else.
To check the relay outside Haven entirely, put your TURN URL and credentials into the WebRTC project's Trickle ICE page (https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/). If it never produces a candidate of type
relay, the problem is coturn or the firewall in front of it, not Haven.
coturn is not the only option. eturnal does the same job and several people running Haven at home have found it easier to get working. The setting that matters is the same one under a different name:
relay_ipv4_addrineturnal.ymlis coturn'sexternal-ip, and its autodetection does not reliably find your public address behind NAT, so set it explicitly there too. Everything else on this page (UDP forwarding, the relay port range, NAT reflection) applies unchanged, and Haven does not care which one you pointTURN_URLat.
Rich Presence (What You're Playing & Listening To)
Haven can show your current game or track next to your name in the member list, and on your profile card. Games take priority in the member list so the sidebar stays readable; your profile card shows both if you're doing both.
This is off until you turn it on. Nothing is shared until you link an account.
Linking your accounts
- Click ⚙️ Settings → Connections
- Pick a source and follow the prompt:
| Source | What you do | What it covers |
|---|---|---|
| Haven's music player | Nothing, it just works | Anything playing in a Haven voice channel |
| Last.fm ⭐ | Enter your username | Spotify, Apple Music, YouTube Music, Navidrome, Plex |
| Steam | Click Link and sign in | Games |
| Spotify | Click Link and sign in | Spotify only |
Last.fm is the one to pick for music. It's just a username, there's no sign-in redirect and nothing gets stored, and because most music apps scrobble to Last.fm, that single connection covers whatever you actually listen with.
Scrobbling has to be turned on in Last.fm's own settings first. Haven's setup panel walks you through it for each app.
Controlling what people see
- Master switch: turn presence off entirely
- Per-category: show games but not music, or the other way round
- Invisible: while your status is Invisible, nothing is shared, regardless of the settings above
None of it is written to the database. Presence lives in memory and disappears when the server restarts.
If nothing is showing up
- Give it a minute. Haven checks for updates on a timer rather than instantly.
- For music, confirm the track actually appears on your Last.fm profile. If it isn't scrobbling there, Haven has nothing to read.
- For Steam, your Steam profile's game details must be set to Public. Steam's API returns nothing for private profiles.
- Check that your status isn't set to Invisible.
Admins: each provider needs a free API key before anyone can link to it. Settings → Connections has a Set up button per provider with a link to where that key comes from, and keys are saved without a restart. To swap a key later, use Change key on that row.
Push Notifications
Push notifications let you receive alerts when someone messages a channel you're in, even when the Haven tab is in the background or closed.
Requirements
- HTTPS is required. Push notifications use Service Workers, which only work over
https://orlocalhost. If you're accessing Haven via a LAN IP likehttp://192.168.1.x:3000, push will not work. - A modern browser (Chrome, Edge, Firefox, or Safari 16+)
- Haven must be running with SSL certificates (the default; Haven makes its own)
How to Enable
- Open Haven in your browser via
https://(e.g.,https://localhost:3000orhttps://your-domain:3000) - Click the ⚙️ Settings button (bottom of the right sidebar)
- Scroll to Push Notifications and flip the toggle on
- Your browser will ask for notification permission. Click Allow
- The status should change to Enabled
Setting Up on Your Devices
Desktop (Windows / macOS / Linux):
- Works in Chrome, Edge, and Firefox out of the box
- Make sure you access Haven via
https://(nothttp://) - If you see "Service worker failed" or "Requires HTTPS", you're on an insecure connection
Mobile (Android):
- Open Haven in Chrome or Edge via
https:// - Enable push in Settings (same steps as above)
- Notifications appear even when Chrome is closed
Mobile (iOS / iPadOS):
- Requires Safari 16.4+ (iOS 16.4 or later)
- First, Add to Home Screen: tap Share → "Add to Home Screen"
- Open Haven from the home screen icon (it runs as a web app)
- Enable push in Settings. Safari will ask for permission
Troubleshooting Push
| Problem | Solution |
|---|---|
| "Service worker failed" | You're not on HTTPS. Use https://localhost:3000 or set up SSL certs (see Troubleshooting below) |
| "Requires HTTPS" | Access Haven via https:// instead of http:// |
| "Permission denied" | You blocked notifications. Reset in browser settings: Settings → Site Settings → Notifications → find Haven → Allow |
| Toggle is grayed out | Your browser doesn't support push, or you're in incognito/private mode |
| Notifications not appearing | Check your OS notification settings. Haven notifications may be muted at the system level |
| Only works on localhost | For LAN/remote access, you need valid SSL. Haven generates self-signed certs itself on first start |
Configuration
Haven creates a .env config file automatically on first launch. You do not need
to create or rename anything. It lives in your data directory:
| OS | Data Directory |
|---|---|
| Windows | %APPDATA%\Haven\ |
| Linux / macOS | ~/.haven/ |
| Setting | Default | What it does |
|---|---|---|
PORT |
3000 |
Server port |
SERVER_NAME |
Haven |
Your server's display name |
ADMIN_USERNAME |
admin |
Register with this name to get admin powers |
JWT_SECRET |
(auto-generated) | Security key. Do not share or edit this |
HAVEN_DATA_DIR |
(see above) | Override the data directory location |
HAVEN_DISK_RESERVE_MB |
512 |
Disk space Haven keeps free so a full server stays fixable |
SSL_CERT_PATH |
(auto-detected) | Path to SSL certificate. With Let's Encrypt, point at fullchain.pem rather than cert.pem: cert.pem leaves out the intermediate certificate, which browsers quietly work around but curl and many other clients reject |
SSL_KEY_PATH |
(auto-detected) | Path to SSL private key |
STEAM_API_KEY |
(empty) | Steam Web API key for rich presence. Get yours at steamcommunity.com/dev/apikey (any domain works) |
OIDC_CLIENT_SECRET |
(empty) | Client secret for single sign-on. Kept out of the database on purpose, so backups never carry it |
PUBLIC_URL |
(auto-detected) | Your server's public address, including https://. Only needed if Haven cannot work it out itself, see below |
HAVEN_ALLOW_PRIVATE_CALLBACKS |
false |
Let bot callback URLs point at private addresses (10.x, 192.168.x, localhost, .local). Off by default so a webhook cannot be pointed at your internal network. Turn it on only if your bot really does run on the same LAN or in a sibling Docker container. Cloud metadata addresses (169.254.x.x) stay blocked regardless |
After editing .env, restart the server.
Running Haven as a systemd service? systemd runs the unit as the user you set in
User=(oftenroot), so~/.haven/resolves to that user's home (e.g./root/.haven/), not the directory you ran Haven from manually during testing. SetHAVEN_DATA_DIRto an absolute path in your.envor the unit'sEnvironment=line so manual and service runs share the same data, certs, and.env. Example:Environment=HAVEN_DATA_DIR=/opt/haven-data
Steam or Spotify linking sending people to the wrong address? If you run Haven behind Docker port mapping (say
8080:3000), a reverse proxy that strips the port out of the Host header, or a Cloudflare Tunnel, the server cannot reliably guess its own public address, so those sign-in round-trips fail. Set it explicitly:PUBLIC_URL=https://haven.example.com:8443For security this one is
.env-only and deliberately not editable from the admin panel. A callback address that could be changed from the web UI would be a way to hijack sign-in redirects.
Running More Than One Server
You can run several Haven instances on the same machine. Each one needs its own copy of Haven, its own port, and its own data directory so the databases do not conflict.
- Copy the Haven folder to a separate directory for each server.
- In each copy, set a unique
PORTin.env(e.g.3000,3001). - Set a unique
HAVEN_DATA_DIRin each.env(e.g.HAVEN_DATA_DIR=C:\HavenData\server1). - Start each one independently with
Start Haven.bat(orstart.sh).
Tips
- Bookmark the URL, so you don't have to type the IP every time
- Keep the bat window open: closing it stops the server
- Your data is stored separately: all messages, config, and uploads are in your data directory (
%APPDATA%\Havenon Windows,~/.havenon Linux/macOS), not in the Haven code folder - Back up your data directory: copy it somewhere safe to preserve your chat history
- Channel codes are secrets: treat them like passwords. Anyone with the code can join.
End-to-End Encryption (E2E)
All direct messages in Haven are end-to-end encrypted. The server never has access to the plaintext of your DMs or the keys needed to decrypt them.
How It Works
- When you first log in, your browser generates an ECDH P-256 key pair.
- The private key is encrypted (wrapped) with a key derived from your password using PBKDF2, and the encrypted blob is stored on the server for cross-device sync.
- The server never sees your password-derived wrapping key. It's computed in your browser and never transmitted.
- When you message someone, both users' public keys are combined via ECDH + HKDF to produce a shared AES-256-GCM encryption key. Messages are encrypted before leaving your browser.
When Keys Are Preserved (Old Messages Readable)
| Scenario | Why it works |
|---|---|
| Close the tab and reopen it | IndexedDB still has your keys; no password needed |
| Refresh the page | Same: IndexedDB survives refreshes |
| JWT auto-login (return visit) | IndexedDB has the keys cached |
| Log in on a new device/browser | You type your password → wrapping key is derived → server backup is downloaded and unwrapped |
| Clear cookies (but NOT site data) | IndexedDB is site data, not cookies, so keys survive |
| Change your password | Private key is re-wrapped with the new password and re-uploaded (the ECDH key pair itself doesn't change) |
When Keys Are Lost (Old Messages Permanently Unreadable)
| Scenario | Why keys are lost |
|---|---|
| Clear all browser/site data when that's your only device | IndexedDB is wiped. On re-login the server backup may still unwrap if password hasn't changed |
| Clear browser data after changing your password | Server backup was wrapped with the old password; new password can't unwrap it → new keys generated |
| Manually reset encryption keys (🔄 button in DM header) | Intentional wipe: new key pair, old messages unreadable |
| Admin deletes your account or resets the database | Server backup gone. If IndexedDB is also empty, fresh keys are generated |
Short version: Same password + at least one of (IndexedDB or server backup) = keys survive. Lost both = old messages gone forever.
Can Anyone Intercept Messages?
| Attack vector | Can they read messages? | Why |
|---|---|---|
| Server admin reading the database | No | Encrypted private key is wrapped with a key derived from YOUR password; admin has the blob but not the key |
| Someone with physical server access | No | Same reason: the blob is useless without your password |
| Man-in-the-middle on the network | No | Messages are encrypted client-side before transmission |
| Stolen JWT token | No | JWT authenticates you, but E2E keys live in your browser's IndexedDB. Attacker can't unwrap the server backup without your password |
| Someone who knows your password + has your JWT | Yes | Equivalent to using your login: they can derive the wrapping key and decrypt everything |
| Modified server JavaScript | Yes | If the admin pushes tampered JS that exfiltrates keys, all bets are off. This is true of every web-based E2E system |
Resetting Encryption Keys
In any DM conversation, click the 🔄 button in the channel header to reset your encryption keys. This:
- Generates a brand new key pair
- Makes all previous encrypted messages permanently unreadable for both parties
- Posts a timestamped notice in the chat so both users know when/why old messages became unreadable
- Requires you to type RESET to confirm (there is no undo)
Verifying Encryption
Click the 🔐 button in the DM header to view your safety number, a 60-digit code derived from both users' public keys. Compare it with your conversation partner through a separate channel (phone, in person, etc.). If they match, no one is intercepting your conversation.
Ferry (Discord Bridge)
Ferry relays messages between your Haven channels and Discord channels. Haven users appear on Discord under their own names, and Discord messages show up in Haven.
The same bot can also copy your Discord server's history into Haven: see Importing from Discord.
Every Haven server needs its own Discord bot. Haven cannot ship a shared one: Discord caps unverified applications at 100 servers and verification requires a company review. Setting one up takes a couple of minutes and is free.
1. Create the Discord bot
- Go to discord.com/developers/applications and click New Application
- Open the Bot tab
- Turn on Message Content Intent. Without it Discord sends every message with an empty body, so nothing reaches Haven
- If you want the Discord DM feature, also turn on Server Members Intent
- Click Reset Token, then copy the token. Treat it like a password
2. Connect it to Haven
- In Haven, go to Settings → Admin → Ferry
- Click Set up Ferry and paste the token
- Haven checks the token with Discord and then shows an invite link
- Open the invite link and add the bot to your Discord server. Keep the Manage Webhooks permission checked: it is what lets relayed messages carry the Haven author's name and picture instead of all arriving as one anonymous bot
- Back in Haven, turn on the Ferry is on switch
Private channels. Discord keeps private channels hidden from the bot until its role is added to them, even when the bot can see everything else on the server. Haven marks those channels with a 🔒 when you pair or import. For each private channel you want bridged or imported:
- In Discord, right-click the channel and pick Edit Channel
- Open Permissions. Under Advanced permissions, click + and pick the bot's role (it has the same name as the bot)
- Set View Channel and Read Message History to the green check, then save
Don't give the bot Administrator to get around this. It works, but then anyone who gets hold of the bot's token controls your whole Discord server.
3. Pair some channels
A pairing joins one Haven channel to one Discord channel. Each pairing has two settings:
| Setting | Options |
|---|---|
| Direction | Two-way, Haven → Discord only, or Discord → Haven only |
| Outgoing | On command (only messages addressed with =>) or Mirror everything (every message in the channel) |
Pairings are also the boundary: members can only send to Discord channels paired with the Haven channel they are in. They cannot reach other servers the bot happens to belong to.
Forums
A Haven forum channel pairs with a Discord forum (or media) channel, and only with one. A chat channel pairs with a Discord text or announcement channel. The pairing form only offers the matching kind once you pick the Haven channel, and Haven refuses a mixed pair.
- A new post on Discord becomes a new topic in the Haven forum: the post's name is the title, its first message is the body, and any tags with the same name in both forums carry over. Messages in the post become replies in the topic
- A new topic in Haven becomes a new post in the Discord forum, under the author's Haven name and picture, with the shared tags. Replies in the topic go into that post
- Forum pairings always carry every post and reply, so they have no Outgoing setting
- Only posts made after you pair cross. Replies in older posts stay on their own side
- Editing the first message or a reply on Discord updates the Haven copy. For a topic that started on Discord, renaming or retagging the post updates the topic, and locking it closes the topic. Edits made in Haven stay in Haven, the same as chat messages
- Deleting a post on Discord unlinks it, and closes its Haven topic if the topic started on Discord. Deleting a topic in Haven unlinks it too, and the Discord post stays. Removing the pairing forgets every link between its topics and posts
- If the Discord forum requires a tag on every post, give the Haven forum a tag with the same name as one of the Discord tags and use it on topics. Tags only Discord moderators may set are never applied from Haven
Forums need nothing extra on Discord: no new intent and no new permission. The bot needs to see the forum and have Manage Webhooks there, the same as for a text channel.
4. Grant the permission
Sending to Discord needs the Send to Discord (Ferry) role permission
(use_ferry), granted under Settings → Roles. Admins always have it. Without it a
member's messages stay in Haven even in a mirrored channel.
Sending a message
In a mirrored channel, just talk. Everything crosses.
In an on command channel, start the message with => and pick a destination from
the autocomplete:
=>My Server#general hey everyone
The => prefix is stripped before the message is stored, and a small badge on the
message shows where it went. If the destination does not match a pairing, the prefix
stays visible so you can see it did not send.
To DM a Discord user (when the admin has enabled it), type =>@ and search by name:
=>@Alice quick question
Things worth knowing
- DMs are one way. A bot cannot impersonate in a DM, so the message arrives from the bot with your Haven name in the body. Replies stay in Discord and do not come back to Haven. Discord also flags bots that DM a lot of people, so use it sparingly
- Pings are off by default. Relayed messages do not ping anyone on Discord unless an
admin turns on Allow pings. Even then
@everyoneand role pings stay blocked - Pictures and files sent in Haven are uploaded to Discord with the message, up to
Discord's 10 MB a file and ten files a message. A bigger one shows as a note instead.
Attachments sent with a
=>target go to that target (or that Discord DM) too, and a spoilered picture arrives as a Discord spoiler - Set
PUBLIC_URLin your.envif you want Haven avatars to appear on the Discord side, and files too big for Discord to show as links. Discord has to be able to reach them over the internet. Without it names still come through, just without avatars - Discord attachments arrive as links, and Discord's own links expire after about a day. The text of the message is permanent, the pictures are not
- Other Discord bots are ignored unless an admin turns on Relay other bots, which can flood a channel
- Personas are off by default. With them off, a relayed message always carries the sender's real Haven name so a Discord server cannot be addressed by an untraceable alias
If it stops working
The Ferry panel shows the connection state and the last error on each pairing.
| Message | Fix |
|---|---|
| Discord refused the Message Content intent | Turn it on in the Developer Portal, Bot tab |
| Discord rejected the bot token | Reset the token in the portal and paste the new one |
| The bot needs "Manage Webhooks" | Give the bot that permission in the Discord channel |
| Nothing ever arrives from a private Discord channel | Add the bot's role to that channel (see Private channels) |
| Discord refused the Server Members intent | Turn it on in the portal, or leave DMs off |
| This pairing joins a forum to a channel that is not a forum | A paired Haven channel was switched to or from a forum. Remove the pairing and pair matching kinds |
| That Discord forum requires a tag on every post | Add a Haven forum tag named like one of the Discord forum's tags and tag the topic with it |
Slash Commands & Shortcuts
Type / in the message box to see the full list with descriptions. A selection:
| Command | What it does |
|---|---|
/gif <query> |
Search and send a GIF inline |
/play <name or url> |
Search and play music in the voice channel |
/poll [question] |
Open the poll creator |
/time 8pm |
Insert a time everyone reads in their own timezone |
/roll 2d20 |
Roll dice (any NdN format) |
/flip |
Flip a coin |
/me does something |
Italic action text |
/spoiler secret text |
Hidden spoiler text |
/tts hello |
Text-to-speech (/tts:stop to stop playback) |
/nick NewName |
Change your username |
/hug @user |
Send someone a hug |
/shrug /tableflip /unflip /lenny /disapprove |
Text faces |
/afk /brb /bbs |
Away announcements |
/clear |
Clear your own chat view |
Bots can register their own slash commands too, see the developer guide below.
Timestamps that follow the reader
A message can carry an instant rather than a wall-clock time, so everyone sees it on their own clock. Useful when the group is spread across timezones: you say one time, nobody does the arithmetic, and nobody turns up an hour late.
/time 8pm puts a token like <t:1780853820:f> in your message box. Type around
it and send. Whoever reads it sees their own local time. The clock button next to
the poll button, or /time on its own, opens a picker instead: pick the date and
time, see every style previewed in your own locale, and insert the one you want.
What /time accepts:
| You type | Means |
|---|---|
/time 8pm or /time 20:00 |
Today at that time, or tomorrow if it already went by |
/time tomorrow 9am |
Tomorrow at nine |
/time 2026-09-06 20:30 |
That exact date and time |
/time 2026-09-06 |
Just the date |
/time +2h |
Two hours from now (m, h, d, w all work) |
/time 1780853820 |
A unix timestamp you already have |
Everything you type is read in your timezone. Add a style letter to change
how it looks, for example /time 8pm R:
| Letter | Looks like |
|---|---|
t |
8:17 PM |
T |
8:17:00 PM |
d |
09/06/2026 |
D |
September 6, 2026 |
f (default) |
September 6, 2026 at 8:17 PM |
F |
Sunday, September 6, 2026 at 8:17 PM |
R |
in 3 hours (counts itself down) |
Hovering any of them shows the full date and time spelled out. This is the same syntax Discord uses, so tokens survive the Ferry bridge in both directions and the timestamp generators people already use work here too.
Keyboard shortcuts
| Key | Action |
|---|---|
Shift+Enter |
New line |
Ctrl+F |
Search messages |
@ |
Mention autocomplete |
: |
Emoji autocomplete (type 2+ characters) |
/ |
Slash command autocomplete |
:: |
Persona autocomplete (send as one of your personas) |
Tab |
Accept the highlighted suggestion |
Mentioning a role
@Moderators (or any role name) lights up for everyone who holds that role and
pings them the way an @mention does. It shows up in the @ picker for anyone
with the Mention everyone permission, and the server quietly disarms it for
anyone without, since a role ping reaches a crowd the same way @everyone does.
Anyone who would rather not be pinged by their roles can turn @Role mentions
off under Settings, Sounds.
Admin & Moderation
Admin controls live in Settings (the gear icon in the sidebar). If you registered with the admin username you have all of them; everything below can also be handed to others through the role system, one permission at a time.
- Members: kick, timed mute, ban, unban, delete an account to free the username
- Roles: granular per-channel permissions, including a server-wide "see every channel" permission that only the owner can grant
- Automod: checks links in messages, edits, DMs, profiles and channel topics against a domain policy you control, and rejects them before they are saved. This matters more than it sounds: Haven renders linked images and previews from the other site, so a hostile link exposes the IP of everyone who scrolls past it
- Audit log: a record of channel changes, role updates, bans, kicks and server setting changes, with who did what
- Guests: an optional "Join as Guest" button on the login page. Guests pick a username, get a temporary account with no password, see only the channels you whitelist, cannot DM, and are deleted when they disconnect
- Uploads & limits: max upload size (25 MB by default, raise it as far as your disk allows), attachments per message (10 by default), max message length, per-member storage usage
- Auto-cleanup: automatic deletion of messages past a chosen age, or of the oldest messages once the database or the uploads folder passes a size you set (for uploads, only messages with files go, and pinned and archived ones stay), and how long the files left behind by deleted messages and channels are kept before they are removed for good (a week by default)
- Server updates: check for a new Haven release and apply it in place. Haven takes a pre-update backup and restarts itself
Backing Up Your Data
All your data lives in a dedicated directory outside the Haven code folder:
| OS | Location |
|---|---|
| Windows | %APPDATA%\Haven\ |
| Linux / macOS | ~/.haven/ |
Inside you will find haven.db (messages, users, channels), .env, certs/, and
uploads/. Copying that whole folder somewhere safe backs up everything. The Haven
code directory holds no personal data.
Built-in backups
You do not have to copy files by hand. Settings → Admin → Backup has:
- One-click export: download an archive, with checkboxes for what to include (channels and roles, users, server settings, messages, uploaded files, and optionally DMs)
- Scheduled auto-backups: on a daily or weekly schedule
- Restore: upload a backup to restore a server. The previous database and
uploads are kept as
.pre-restorecopies for one cycle as a safety net - Restore on a new machine: the setup wizard (
Install Haven.baton Windows,install.shelsewhere) has Restore from a backup on its first page, so moving Haven to another computer brings your accounts back without creating a new admin first. The backup needs Messages ticked
Backups stream to and from disk rather than being held in memory, so a large uploads folder will not run the server out of RAM.
GIF Search Setup
Haven's GIF picker is powered by GIPHY and needs a free API key.
- Go to developers.giphy.com
- Create an account (or sign in)
- Click Create an App → choose API
- Name it anything (e.g. "Haven Chat")
- Copy the API key
- In Haven, as an admin, click the GIF button (🎞️) in the message box and paste the key into the setup prompt
The key is stored server-side, so only admins can see or change it, and every user can search GIFs once it is set. No payment is involved; GIPHY's free tier is far more than a private server will use. Tenor is no longer supported for new setup; a server that already has a Tenor key keeps working until a GIPHY key is set.
KLIPY works as well. Get a key at klipy.com/developers
and set KLIPY_API_KEY in your .env (or Docker environment). If more than one key
is set, PREFERRED_GIF_SEARCH (klipy, giphy or tenor) picks which provider the
picker uses; without it the server tries GIPHY first, then KLIPY, then Tenor.
Translations
Haven ships in 11 languages: English, French, German, Spanish, Polish, Russian, Chinese, Brazilian Portuguese, Persian (فارسی), Japanese (日本語) and Korean (한국어). Users pick one in Settings → Language or on the login page, and the choice is saved per browser. A first visit follows the browser's language.
English is the reference. Polish, Russian and Brazilian Portuguese have been reviewed by native speakers; the rest started as machine translations and still need a pass, so corrections are genuinely welcome. Persian, Japanese and Korean cover about two thirds of the app so far (chat, voice, forums, DMs and the common settings); the rest shows in English until someone fills it in.
Improving a language: edit public/locales/{code}.json and open a PR.
Adding one:
- Copy
public/locales/en.jsontopublic/locales/{code}.json - Translate the values, leaving the keys unchanged
- Fill in the
_metablock with the language name and flag - Add the code to the
SUPPORTEDarray inpublic/js/i18n.js - Add an
<option>to the language selectors inpublic/index.html,public/app.htmlandpublic/theme-preview.html - Add the code to the server's allowed list (
validLocalesinsrc/socketHandlers/admin.jsanddefault_localeinsrc/serverTemplate.js)
Missing keys fall back to English per phrase, so a partial translation never breaks anything. If you would like to own a language and keep it current, say so in an issue.
Bot & Webhook Developer Guide
Haven has a built-in bot API powered by webhooks. Bots can send messages, delete messages, play soundboard sounds, and register custom slash commands.
Looking for ready-made bots and webhooks? The community library at ancsemi/haven-community collects user-contributed integrations you can deploy as-is (a GitHub releases poster, etc.). PRs welcome there if you've built one of your own. See its
CONTRIBUTING.mdfor how to submit.
Creating a Bot
- Go to Settings → Admin → Bots (or open a channel's settings and look for the webhook/bot option)
- Create a new webhook. Give it a name, optionally set an avatar URL and a callback URL
- Copy the Webhook Token (64-character hex string). This is your bot's API key
Sending Messages
POST https://your-server.com/api/webhooks/<token>
Content-Type: application/json
{
"content": "Hello from my bot!",
"username": "MyBot",
"avatar_url": "https://example.com/avatar.png"
}
content(required): message text, max 4000 charactersusername(optional): override the bot's display name for this message (max 32 characters; control characters and<>are removed)avatar_url(optional): override the bot's avatar for this message. It must be anhttps://link or one of this server's/uploads/files; anything else is dropped and the message posts with no avatarephemeral(optional): whentrue, deliver only torecipient_idand do not store in historyrecipient_id(required whenephemeralistrue): user id that should receive the private bot messagereply_to(optional): id of a message in the same channel to reply tothread_id(optional): id of a top-level message in the bot's channel; the message is posted as a reply inside that thread (not combinable withephemeral)silent(optional): whentrue, the message makes no sound, pop-up or push for anyone; startingcontentwith@silentdoes the same
Ephemeral example:
POST https://your-server.com/api/webhooks/<token>
Content-Type: application/json
{
"content": "Your dashboard token: abc123",
"ephemeral": true,
"recipient_id": 42
}
Thread example:
POST https://your-server.com/api/webhooks/<token>
Content-Type: application/json
{
"content": "Build finished",
"thread_id": 123
}
Response (regular): { "success": true, "message_id": 124 } (plus "thread_id": 123 for a thread reply)
Response (ephemeral): { "success": true, "ephemeral": true, "recipient_id": 42, "delivered": true }
Deleting Messages
DELETE https://your-server.com/api/webhooks/<token>/messages/<message_id>
Bots can delete any message in their assigned channel. Returns { "success": true }.
Bots with moderation permission can delete up to 100 of the most recent messages from their assigned channel in one request. Thread replies are removed with their parent messages.
DELETE https://your-server.com/api/webhooks/<token>/messages?limit=25
Returns { "success": true, "deleted": 25 }. The deleted count includes
thread replies removed with the selected messages.
Playing Soundboard Sounds
POST https://your-server.com/api/webhooks/<token>/sounds
Content-Type: application/json
{
"sound": "AOL - You've Got Mail"
}
Plays the named sound for all users currently viewing the bot's channel. Use GET /api/sounds (with a Bearer token) to list available sound names.
Registering Slash Commands
Bots with a callback_url can register custom slash commands that users can invoke from chat:
Register:
POST https://your-server.com/api/webhooks/<token>/commands
Content-Type: application/json
{
"command": "leaderboard",
"description": "Show the current leaderboard"
}
Optional subcommands can be included to improve autocomplete discoverability:
POST https://your-server.com/api/webhooks/<token>/commands
Content-Type: application/json
{
"command": "rss",
"description": "Manage RSS feeds",
"subcommands": [
{ "name": "add", "description": "Add an RSS feed" },
{ "name": "remove", "description": "Remove an RSS feed" },
{ "name": "list", "description": "List active feeds" }
]
}
The callback payload format is unchanged. Haven still sends command as the
base command (rss) and the full remaining text in args (for example
"add https://example.com/feed.xml").
List:
GET https://your-server.com/api/webhooks/<token>/commands
Unregister:
DELETE https://your-server.com/api/webhooks/<token>/commands/leaderboard
When a user types /leaderboard, Haven sends a POST to your bot's callback URL with the command details, signed with HMAC so you can verify authenticity.
Rate Limits
All webhook endpoints are rate-limited to 30 requests per minute per IP.
Callback Payloads
If your webhook has a callback_url and callback_secret configured, Haven will POST command invocations to your URL. The payload includes an HMAC signature in the X-Haven-Signature header that you should verify using your callback secret.
Troubleshooting
"SSL_ERROR_RX_RECORD_TOO_LONG" or "ERR_SSL_PROTOCOL_ERROR" in browser
→ Your browser is trying to connect via https:// but the server is actually running in HTTP mode. Haven makes its own self-signed certificate on first start (no OpenSSL needed), so this now only happens when FORCE_HTTP=true is set in your .env, or when the certificate files in your data directory are unreadable.
Quick fix: Change the URL in your browser from https://localhost:3000 to http://localhost:3000.
Permanent fix:
- Open
.envin your data directory (%APPDATA%\Havenon Windows,~/.havenelsewhere) and removeFORCE_HTTP=trueunless a reverse proxy is terminating TLS for you - If the startup log says the certificate could not be loaded, delete the
certsfolder in that data directory - Re-launch
Start Haven.bat. Haven regenerates the certificate and starts in HTTPS mode
How to tell if you're running HTTP or HTTPS:
Check the server's startup banner in the terminal. If it says http://localhost:3000, you're on HTTP. If it says https://localhost:3000, you're on HTTPS. The protocol in the URL you use must match.
"Node.js is not installed" → Download and install from nodejs.org. Restart your PC after installing.
Server starts but browser shows blank page → Try clearing your browser cache, or open in an incognito/private window.
Friends can connect locally but not remotely → Port forwarding isn't configured correctly. Double-check the port, protocol, and internal IP.
"Error: EADDRINUSE"
→ Another program is using port 3000. Close it, or change the port in .env.
Voice chat echoes → Use headphones to prevent your speakers from feeding into your microphone.
⬡ Haven: Your server. Your rules.