Home Features Screenshots Downloads Docs FAQ GitHub

Documentation

Everything you need to deploy, configure, and extend Bastion.

Getting Started

Bastion is designed to be easy to self-host. The quickest way to get started is with Docker Compose, which sets up the entire stack — backend, database, and cache — in a single command.

Terminal
git clone https://github.com/Calmingstorm/bastion.git
cd bastion
cp .env.example .env    # Edit with your settings
docker compose up -d

That's it! Open http://localhost:8080 in your browser to access your Bastion instance. The first user to register becomes the server owner.

Requirements

Minimum requirements for self-hosting:

Component Minimum Recommended
CPU1 core2+ cores
RAM512 MB1+ GB
Storage1 GB10+ GB (for file uploads)
Docker20.10+Latest
Docker Composev2.0+Latest

Bastion runs well on budget VPS providers like Linode, DigitalOcean, or Hetzner. A $5/month instance is sufficient for small communities.

Installation

Detailed installation steps:

1. Clone the repository

Terminal
git clone https://github.com/Calmingstorm/bastion.git
cd bastion

2. Configure environment

Terminal
cp .env.example .env

Edit .env with your preferred settings. At minimum, change BASTION_JWT_SECRET to a random string. See Configuration for all options.

3. Start the stack

Terminal
docker compose up -d

4. Verify

Terminal
# Check all containers are running
docker compose ps

# View logs
docker compose logs -f bastion

Configuration

Bastion is configured via environment variables. All variables support a BASTION_ prefix. Where noted, a shorter fallback name is also accepted.

Server

VariableDefaultDescription
BASTION_HOST0.0.0.0HTTP server bind address
BASTION_PORT8080HTTP server port
BASTION_DOMAINhttp://localhost:5173Frontend URL (used in password reset emails)

Database (PostgreSQL)

VariableFallbackDefaultDescription
BASTION_DB_HOSTDB_HOSTlocalhostPostgreSQL host
BASTION_DB_PORTDB_PORT5432PostgreSQL port
BASTION_DB_NAMEDB_NAMEbastionDatabase name
BASTION_DB_USERDB_USERbastionDatabase user
BASTION_DB_PASSWORDDB_PASSWORDbastionDatabase password

JWT Authentication

VariableFallbackDefaultDescription
BASTION_JWT_SECRETJWT_SECRETSecret for signing JWT tokens (required)
BASTION_JWT_ACCESS_TTLJWT_ACCESS_TTL15mAccess token lifetime
BASTION_JWT_REFRESH_TTLJWT_REFRESH_TTL168hRefresh token lifetime (default 7 days)

Redis

VariableFallbackDefaultDescription
BASTION_REDIS_HOSTREDIS_HOSTlocalhostRedis host
BASTION_REDIS_PORTREDIS_PORT6379Redis port

File Uploads

VariableDefaultDescription
BASTION_UPLOAD_DIR./uploadsFile upload storage path
BASTION_UPLOAD_MAX_SIZE10MBMaximum file upload size (supports KB, MB, GB)
BASTION_UPLOAD_BASE_URL/api/uploadsBase URL for serving uploaded files

Email (SMTP)

SMTP is checked first for sending emails. Configure either SMTP or Mailgun (or both as fallback).

VariableDefaultDescription
BASTION_SMTP_HOSTSMTP server host
BASTION_SMTP_PORT587SMTP server port
BASTION_SMTP_USERSMTP username
BASTION_SMTP_PASSSMTP password
BASTION_SMTP_FROMBastion <noreply@localhost>From address for outgoing email

Email (Mailgun)

Used as a fallback if SMTP is not configured, or as the primary sender on hosts that block outbound SMTP (e.g. Linode).

VariableDefaultDescription
BASTION_MAILGUN_API_KEYMailgun API key
BASTION_MAILGUN_DOMAINMailgun sending domain
BASTION_MAILGUN_FROMBastion <noreply@localhost>From address for outgoing email

GIF Search (Optional)

Set one of these to enable the GIF picker in the client. Tenor takes priority if both are set. The /api/v1/features endpoint reports whether GIF search is enabled.

VariableDescription
BASTION_TENOR_API_KEYGoogle Cloud API key with Tenor API enabled (free tier: 5000 req/day)
BASTION_GIPHY_API_KEYGiphy API key (free for non-commercial use)

Reverse Proxy

For production deployments, use a reverse proxy with automatic HTTPS. WebSocket support is required for real-time features. Here's a recommended Caddy configuration:

Caddyfile
chat.yourdomain.com {
    reverse_proxy localhost:8080
}

Caddy handles WebSocket upgrades and TLS automatically. Nginx alternative:

nginx.conf
server {
    server_name chat.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Bot Development

Bastion supports bots with full API access. Bots authenticate with Authorization: Bot <token> and have their own user identity with the same role and permission system as regular users. Bot messages display a BOT badge.

Creating a Bot

  1. Go to Server Settings → Integrations → Bots
  2. Click "Create Bot" and give it a name
  3. Copy the bot token (prefixed with bot_) — keep it secret!
  4. The bot token is hashed with argon2id and cannot be recovered. Use "Regenerate Token" if lost.

Authentication

Bot requests use a different auth scheme than user JWTs. Include the token in the Authorization header:

HTTP Header
Authorization: Bot bot_your_token_here

Example: Sending a Message

Python
import requests

TOKEN = "bot_your_token_here"
BASE_URL = "https://your-instance.com/api/v1"

# Send a message to a channel
response = requests.post(
    f"{BASE_URL}/channels/{channel_id}/messages",
    headers={"Authorization": f"Bot {TOKEN}"},
    json={"content": "Hello from my bot!"}
)

WebSocket Events

Bots can connect to the WebSocket at /api/v1/ws using the same Bot token scheme to receive real-time events such as new messages, member joins, and interaction dispatches.

Webhooks

Webhooks let you send messages to channels from external services without a bot user. Create a webhook in Server Settings → Webhooks, then POST to the webhook URL. Webhook tokens are prefixed with whk_.

cURL
curl -X POST "https://your-instance.com/api/v1/webhooks/{id}/{token}" \
  -H "Content-Type: application/json" \
  -d '{"content": "Deployment successful!", "username": "CI Bot"}'

Webhook execution is rate-limited to 30 requests per minute per IP. No authentication header is needed — the token in the URL serves as the credential.

Slash Commands

Bots can register slash commands that users invoke with / in the message input. Bastion supports three command types:

  • Type 1 — Slash commands (e.g. /ping)
  • Type 2 — User context menus (right-click a user)
  • Type 3 — Message context menus (right-click a message)

Registering a Command

Python
# Register a slash command for your bot
requests.post(
    f"{BASE_URL}/servers/{server_id}/bots/{bot_id}/commands",
    headers={"Authorization": f"Bot {TOKEN}"},
    json={
        "name": "ping",
        "description": "Check if the bot is alive",
        "type": 1
    }
)

Handling Interactions

When a user invokes a command, the server creates an interaction token (15-minute TTL) and dispatches an INTERACTION_CREATE event to your bot via WebSocket. Your bot responds by calling the interaction callback endpoint:

Python
# Respond to an interaction
requests.post(
    f"{BASE_URL}/interactions/{interaction_token}/callback",
    json={
        "content": "Pong! Bot is alive.",
        "ephemeral": True  # Only visible to the invoker
    }
)

If the bot has no active WebSocket connections when a command is invoked, the server returns a 503 BOT_OFFLINE error to the user.

API Overview

Bastion exposes a RESTful API at /api/v1/. All endpoints accept and return JSON. The legacy /api/ prefix redirects to /api/v1/ for backward compatibility.

The server also provides auto-generated API documentation at /api/v1/docs and an OpenAPI 3.0 specification at /api/v1/docs/openapi.yaml.

Authentication

Protected endpoints require an Authorization header. Bastion supports two schemes:

  • Bearer <jwt_access_token> — for user sessions
  • Bot <bot_token> — for bot integrations

Rate Limiting

ScopeLimitApplies To
Authentication5 req/min per IPLogin, register, password reset
Protected routes120 req/min per userAll authenticated endpoints
Message send10 req/10 sec per userPOST messages
File upload5 req/min per userPOST messages with attachments
Bulk import10 req/min per userChannel message import
Webhooks30 req/min per IPWebhook execution
Interaction callbacks30 req/min per IPBot interaction responses

Authentication Endpoints

All auth endpoints are public (no token required).

MethodEndpointDescription
POST/api/v1/auth/registerCreate a new account
POST/api/v1/auth/loginLogin and receive access + refresh tokens
POST/api/v1/auth/refreshRefresh an expired access token
POST/api/v1/auth/forgot-passwordRequest a password reset email
POST/api/v1/auth/reset-passwordReset password with a token from email
GET/healthHealth check
GET/api/v1/featuresFeature flags (GIF search enabled, provider name)

Users

MethodEndpointDescription
GET/api/v1/users/meGet current user profile
PATCH/api/v1/users/meUpdate profile (display name, about me, status)
POST/api/v1/users/me/avatarUpload avatar image
POST/api/v1/users/me/change-passwordChange password
POST/api/v1/users/me/change-emailChange email address
DELETE/api/v1/users/meDelete account
GET/api/v1/users/me/read-statesList unread channel states
GET/api/v1/users/{userID}Get a user's public profile
GET/api/v1/users/search?q=...Search users (shared server members)

Servers

MethodEndpointDescription
POST/api/v1/serversCreate a server
GET/api/v1/serversList servers you've joined
GET/api/v1/servers/{id}Get server details
PATCH/api/v1/servers/{id}Update server settings
DELETE/api/v1/servers/{id}Delete server (owner only)
POST/api/v1/servers/{id}/iconUpload server icon
POST/api/v1/servers/{id}/joinJoin a server
DELETE/api/v1/servers/{id}/leaveLeave a server
GET/api/v1/servers/{id}/membersList server members
PATCH/api/v1/servers/{id}/members/{userID}/nicknameUpdate a member's nickname
POST/api/v1/servers/{id}/invitesCreate an invite link
GET/api/v1/servers/{id}/invitesList server invites
GET/api/v1/servers/{id}/audit-logView audit log

Channels & Categories

MethodEndpointDescription
GET/api/v1/servers/{id}/channelsList channels in a server
POST/api/v1/servers/{id}/channelsCreate a channel
PATCH/api/v1/servers/{id}/channels/{channelID}Update channel (name, topic)
DELETE/api/v1/servers/{id}/channels/{channelID}Delete a channel
PUT/api/v1/servers/{id}/channels/reorderReorder channels (drag-and-drop)
GET/api/v1/servers/{id}/categoriesList categories
POST/api/v1/servers/{id}/categoriesCreate a category
PATCH/api/v1/servers/{id}/categories/{categoryID}Update a category
DELETE/api/v1/servers/{id}/categories/{categoryID}Delete a category

Messages

MethodEndpointDescription
GET/api/v1/channels/{channelID}/messagesGet messages (cursor-based pagination)
POST/api/v1/channels/{channelID}/messagesSend a text message
POST/api/v1/channels/{channelID}/messages/uploadSend a message with file attachments
PUT/api/v1/channels/{channelID}/messages/{messageID}Edit a message
DELETE/api/v1/channels/{channelID}/messages/{messageID}Delete a message
POST/api/v1/channels/{channelID}/importBulk import messages (migration tool)
PUT/api/v1/channels/{channelID}/messages/{messageID}/reactions/{emoji}Add a reaction
DELETE/api/v1/channels/{channelID}/messages/{messageID}/reactions/{emoji}Remove a reaction
PUT/api/v1/channels/{channelID}/pins/{messageID}Pin a message (max 50 per channel)
DELETE/api/v1/channels/{channelID}/pins/{messageID}Unpin a message
GET/api/v1/channels/{channelID}/pinsList pinned messages
POST/api/v1/channels/{channelID}/ackMark channel as read
GET/api/v1/uploads/*Serve uploaded files (public)

Direct Messages

MethodEndpointDescription
POST/api/v1/dmCreate or get a DM channel (1:1 or group)
GET/api/v1/dmList your DM channels
GET/api/v1/dm/{channelID}Get a DM channel
POST/api/v1/dm/{channelID}/closeClose (hide) a DM channel

Roles & Permissions

Permissions use a bitfield system with per-channel overrides. The computed permissions endpoint returns a member's effective permissions after applying role hierarchy and channel-level overrides.

MethodEndpointDescription
GET/api/v1/servers/{id}/rolesList roles
POST/api/v1/servers/{id}/rolesCreate a role
PATCH/api/v1/servers/{id}/roles/{roleID}Update role (name, color, permissions)
DELETE/api/v1/servers/{id}/roles/{roleID}Delete a role
POST/api/v1/servers/{id}/roles/{roleID}/assignAssign a role to a member
POST/api/v1/servers/{id}/roles/{roleID}/removeRemove a role from a member
GET/api/v1/servers/{id}/permissionsGet a member's computed permissions

Moderation

MethodEndpointDescription
POST/api/v1/servers/{id}/kick/{targetID}Kick a member
POST/api/v1/servers/{id}/bans/{targetID}Ban a member
DELETE/api/v1/servers/{id}/bans/{targetID}Unban a member
GET/api/v1/servers/{id}/bansList banned members
POST/api/v1/servers/{id}/timeout/{targetID}Timeout a member (with duration)

Invites

MethodEndpointDescription
DELETE/api/v1/invites/{inviteID}Delete an invite
POST/api/v1/invites/{code}/joinJoin a server via invite code

Bots API

MethodEndpointDescription
POST/api/v1/servers/{id}/botsCreate a bot
GET/api/v1/servers/{id}/botsList bots in a server
GET/api/v1/servers/{id}/bots/{botID}Get a bot
PATCH/api/v1/servers/{id}/bots/{botID}Update a bot
DELETE/api/v1/servers/{id}/bots/{botID}Delete a bot
POST/api/v1/servers/{id}/bots/{botID}/regenerate-tokenRegenerate bot token

Webhooks API

MethodEndpointAuthDescription
POST/api/v1/servers/{id}/webhooksYesCreate a webhook
GET/api/v1/servers/{id}/webhooksYesList webhooks
GET/api/v1/servers/{id}/webhooks/{webhookID}YesGet a webhook
PATCH/api/v1/servers/{id}/webhooks/{webhookID}YesUpdate a webhook
DELETE/api/v1/servers/{id}/webhooks/{webhookID}YesDelete a webhook
POST/api/v1/webhooks/{webhookID}/{token}NoExecute webhook (send a message)

Interactions API

MethodEndpointAuthDescription
POST/api/v1/servers/{id}/bots/{botID}/commandsBotRegister a slash command
GET/api/v1/servers/{id}/bots/{botID}/commandsBotList bot's commands
PATCH/api/v1/servers/{id}/bots/{botID}/commands/{commandID}BotUpdate a command
DELETE/api/v1/servers/{id}/bots/{botID}/commands/{commandID}BotDelete a command
GET/api/v1/servers/{id}/commandsYesList all commands in a server
POST/api/v1/servers/{id}/interactionsYesExecute a slash command
POST/api/v1/interactions/{token}/callbackNoBot responds to an interaction

Search, GIFs & Utilities

MethodEndpointDescription
GET/api/v1/search?q=...&serverId=...&channelId=...Full-text message search
GET/api/v1/gifs/search?q=...&limit=20Search GIFs (Tenor or Giphy)
GET/api/v1/gifs/trending?limit=20Trending GIFs
GET/api/v1/unfurl?url=...Resolve Tenor/Giphy share URLs to media URLs
GET/api/v1/docsInteractive API documentation
GET/api/v1/docs/openapi.yamlOpenAPI 3.0 specification

WebSocket

Real-time events are delivered via WebSocket at /api/v1/ws. Connect with a valid access token or bot token. The connection supports heartbeat/keepalive and automatic reconnection.

JavaScript
const ws = new WebSocket(
  `wss://your-instance.com/api/v1/ws?token=${accessToken}`
);

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log(data.type, data.payload);
};

Events include message create/update/delete, typing indicators, presence changes, member updates, channel changes, interaction dispatches, and more.

Troubleshooting

Container won't start

Check logs with docker compose logs bastion. Common issues: missing BASTION_JWT_SECRET, database not ready yet (restart with docker compose restart bastion), or port already in use.

WebSocket connection fails

Ensure your reverse proxy passes WebSocket upgrade headers. See the Reverse Proxy section for correct Caddy and Nginx configuration.

File uploads not working

Ensure the BASTION_UPLOAD_DIR exists and is writable by the container. Check BASTION_UPLOAD_MAX_SIZE if large files are rejected.

Database migration errors

Migrations run automatically on startup. If they fail, check that your PostgreSQL version is 14+ and the database user has CREATE TABLE permissions.

Password reset emails not sending

Configure either BASTION_SMTP_* or BASTION_MAILGUN_* environment variables. Some VPS providers (e.g. Linode) block outbound SMTP on port 587 — use Mailgun in that case. Check BASTION_DOMAIN is set correctly for the reset link URL.

GIF picker not showing

Set either BASTION_TENOR_API_KEY or BASTION_GIPHY_API_KEY in your environment. The client checks /api/v1/features to determine if GIF search is available.

Bot returns 503 BOT_OFFLINE

Your bot must have an active WebSocket connection to receive and respond to slash command interactions. Ensure your bot process is running and connected to /api/v1/ws.