Skip to content

Maintenance: We are currently setting up all Infrastructure - Please be patient.

Docs menu · Self-hosting
Help

Self-hosting

Run the bot, the website and the database yourself, for development or for your own community.

You do not need this page to use Roleplay+ Tickets. It is for operators who run their own instance, and for development.

#Requirements

  • Node.js 20.11 or newer
  • Docker for PostgreSQL, or PostgreSQL 15 or newer
  • A Discord application with a bot user

#Repository layout

FolderContent
apps/botThe discord.js bot: panels, tickets, transcripts, automations, commands and an internal HTTP API
apps/webNext.js website, docs, dashboard, Discord sign-in, billing and the transcript viewer
packages/sharedConfig schemas with defaults, the variable engine, plans and the API contract
packages/dbDatabase schema, migrations and a demo seed

The bot owns the connection to Discord. The website reads and writes settings in the database and asks the bot, through its internal API, for live server data and to send panels.

#Discord application

  1. Create an application in the Discord developer portal (opens in a new tab).
  2. Under Bot, create the bot and copy its token. Turn on the privileged intents Server Members and Message Content. Prefix and custom commands, automations, transcripts and the owner left check need them.
  3. Under OAuth2, copy the client id and the client secret. Add the redirect ${PUBLIC_URL}/api/auth/callback, for local development http://localhost:3000/api/auth/callback.

#Configuration

Copy .env.example to .env in the repository root. Both apps read it.

.env
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
DISCORD_TOKEN=
# Optional: register slash commands on one server only, instantly
DISCORD_DEV_GUILD_ID=

DATABASE_URL=postgres://tickets:tickets@localhost:5433/tickets

PUBLIC_URL=http://localhost:3000
# 32 or more random characters: openssl rand -hex 32
SESSION_SECRET=
# Discord user ids with access to the platform admin page
PLATFORM_ADMINS=

BOT_API_URL=http://localhost:4100
BOT_API_PORT=4100
BOT_API_SECRET=
# Encrypts whitelabel bot tokens; bot and web need the same value
ENCRYPTION_KEY=

# Optional billing
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_PRICE_MONTHLY=
STRIPE_PRICE_YEARLY=
STRIPE_PRICE_WHITELABEL_MONTHLY=
STRIPE_PRICE_WHITELABEL_YEARLY=

# Optional website extras
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=
NEXT_PUBLIC_SUPPORT_URL=

SESSION_SECRET encrypts the Discord tokens of signed-in users and signs their sessions. BOT_API_SECRET protects the internal API between website and bot. Use long random values for both and never commit them.

ENCRYPTION_KEY encrypts the tokens of whitelabel bots. Without it, SESSION_SECRET is used. Bot and website need the same value, and changing it later makes stored tokens unreadable, so customers would have to paste them again.

#Local development

Terminal
cp .env.example .env
docker compose up -d db        # PostgreSQL on localhost:5433
npm install
npm run db:migrate
npm run commands:deploy        # registers the slash commands
npm run dev                    # bot and website, website on port 3000

Without DISCORD_DEV_GUILD_ID, Discord can take up to an hour to show new slash commands everywhere.

To look at the dashboard without a Discord application, set DEV_LOGIN=1, run npm run seed and open http://localhost:3000/api/auth/dev. This sign-in does not work in production.

#Production

Terminal
docker compose --profile prod up -d --build

This builds the images, runs the migrations once and starts the bot and the website on port 3000. Put a reverse proxy with TLS in front of the website and set PUBLIC_URL to its public address. Only the website must be reachable from the internet. The bot API stays inside the Docker network.

Uploaded panel images are served at ${PUBLIC_URL}/media/… and Discord loads them from there. They only appear in Discord when PUBLIC_URL is a public https address; on a local setup the bot leaves them out, image links still work.

#Billing with Stripe

Billing is optional. Without it, the checkout shows Coming soon and premium keys from the admin page still work.

  1. Create a product with a monthly and a yearly price in Stripe. Put the price ids into STRIPE_PRICE_MONTHLY and STRIPE_PRICE_YEARLY, the secret key into STRIPE_SECRET_KEY.
  2. Add a webhook endpoint ${PUBLIC_URL}/api/billing/webhook with the events checkout.session.completed, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted and invoice.paid.
  3. Put the signing secret of the webhook into STRIPE_WEBHOOK_SECRET.
  4. For the Whitelabel add-on, create a second product with a monthly and a yearly price and put the price ids into STRIPE_PRICE_WHITELABEL_MONTHLY and STRIPE_PRICE_WHITELABEL_YEARLY.

#Scaling

The web app can run as many replicas as you like. The bot scales in clusters: every bot container runs some shards of the main bot and some whitelabel bots, and the web app sends each request to the container that serves the server.

  1. Set BOT_CLUSTER_COUNT to the number of bot containers, for example 3.
  2. Set DISCORD_SHARD_COUNT to a fixed number of shards, a multiple of the cluster count and at least one per 2,500 servers, for example 6.
  3. Run the bot service with that many replicas and the same environment. Each container claims a free cluster slot by itself.
  4. Give the web app the same two values and redeploy both.

The platform admin shows every cluster under Bots. Dokploy setup, rolling deploys and the details are in docs/deployment.md in the repository.

If you run a public instance, you are the operator. Fill in the imprint, privacy policy and terms templates with your details before you open it to others.