Documentation

Run and configure your own instance

One process serves the web client, the REST API, the WebSocket gateway and this public site on a single port. This guide covers the two install paths, the settings that matter, and the deployment details that catch people out. The full reference lives in docs/.

What you need

Trycord runs as a single Node process. The default install needs little:

  • Docker path: Docker with Compose. Nothing else on the host.
  • Manual path: Node 22 or newer, and a place to keep the SQLite file.
  • Either path: a JWT_SECRET. The server has no default for it and will not start without one.

Generate a secret with:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Every instance needs its own value. Changing it later signs everyone out and invalidates outstanding password-reset links.

Install with Docker

This is the recommended path, and the image is built for it.

git clone https://github.com/trycord/.trycord.git
cd .trycord
cp .env.docker.example .env
# edit .env and set JWT_SECRET
docker compose up -d

Three steps, and the only required value is the secret. Then open http://your-host:9971, create the first account, and promote it to platform administrator by adding its username to ADMIN_USERNAMES in .env and restarting.

docker compose logs -f          # follow the boot log
docker compose ps               # health status
curl -fsS localhost:9971/api/health

/api/health answers 200 only when the database is reachable, so it is a real readiness signal rather than "the process is up".

Install by hand

Choose this if you need systemd units, a specific Node version, or a reverse proxied layout that a container makes awkward.

cd backend
npm install
cp .env.example .env
# edit .env: JWT_SECRET, SERVER_HOST_TYPE and a database profile
npm run seed
npm start

The server finds the web client next to itself in the default layout. If yours differs, point it at the directory holding index.html:

TRYCORD_CLIENT_DIR=/srv/trycord npm start

A relative value resolves against backend/. If no client is found, the API still runs and the server lists every path it looked at.

The settings that matter

backend/.env.example documents every variable; .env.docker.example covers the container. The ones worth understanding:

SettingWhat it does
JWT_SECRETSigns sessions and reset tokens. Required, no default.
SERVER_HOST_TYPEexpress when the server is the public endpoint, nginx when a reverse proxy fronts it. Anything else is a startup error.
TRUST_PROXYSet to 1 only when a reverse proxy on the same host fronts the server.
CLIENT_ORIGINComma-separated allowlist of origins allowed to read API responses. Empty means a restricted development profile.
ADMIN_USERNAMESUsernames promoted to platform administrator at boot. Idempotent.
TRYCORD_NAMEThe instance's display name.

Choose a database

The database is a profile, not a different build. SQLite is a single file and is fine for small and mid-size instances; move to MySQL when you outgrow it.

DB_CLIENT=sqlite
DB_FILE=/data/trycord.db

# or
DB_CLIENT=mysql
DB_HOST=db
DB_PORT=3306
DB_NAME=trycord
DB_USER=trycord
DB_PASSWORD=change-me

A database host that is not loopback is refused unless ALLOW_REMOTE_DB is exactly true, so a CI job that inherits a copied .env cannot quietly write to a live database.

Configure mail

Mail powers email verification and password recovery, and nothing else depends on it. There are three modes:

  • disabled (the default): nothing is sent, and recovery flows refuse rather than claim success.
  • smtp: delivery through a relay. Port 465 implies TLS, 587 implies STARTTLS.
  • api: the server posts JSON to an HTTPS endpoint you name field by field, so no vendor is baked in.

With mail off, email verification is not enforceable and the app will not ask anyone to verify an address.

Put it behind a reverse proxy

Forward both HTTP and WebSocket traffic, set SERVER_HOST_TYPE=nginx and TRUST_PROXY=1, and keep the upgrade headers, or the realtime gateway will not connect:

location / {
    proxy_pass http://127.0.0.1:9971;
    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 7d;
}

The first WebSocket frame carries a single-use ticket, so a short idle timeout looks like a silent disconnect every minute. Set the read timeout generously.

Who may call the API

CLIENT_ORIGIN is a comma-separated allowlist. Leave it empty and the server runs a restricted development profile; set it and the allowlist is enforced exactly, which is what a public instance wants. Three forms are accepted:

EntryMatches
https://trycord.examplethat exact origin only
http://*.trycord.exampleany single subdomain, not the apex
trycord://appthe desktop app

Scheme and port must match exactly, a wildcard never bridges them, and a bare * is rejected. Always include trycord://app if the desktop client should reach the instance.

Back up and upgrade

With Docker, your data lives in two named volumes that survive docker compose down and upgrades: trycord-data for the database and trycord-uploads for avatars and attachments. Back up a volume by copying it out:

docker run --rm -v trycord-data:/data -v "$PWD":/backup \
  alpine tar czf /backup/trycord-data.tar.gz -C /data .

To upgrade, pull and rebuild. Your volumes are untouched.

git pull
docker compose build --pull
docker compose up -d

Point the desktop app at it

The packaged desktop app picks its backend at launch. Give it yours and it runs against that instance and nothing else:

Trycord --api-url=https://chat.example.org

The shipped build reaches the official instance by default. That is a default, not a requirement. See the download page for how the app is put together.

Run a status monitor

The status page at /status works on its own: it probes the instance from the reader's browser. You can also run the bundled status monitor, a second process on its own port that checks the instance on a schedule and serves a report. Because it is a separate process, it keeps answering when the API cannot.

cd backend
npm run status-monitor

It checks /, /health and /api/health by default, and exposes an HTML dashboard, /status.json and Prometheus metrics. Every setting is configurable, by environment variable or by a JSON file. Set STATUS_MONITOR_PUBLIC_URL on the server to have the public status page read the monitor's live report instead of probing from the browser.

With Docker, start the same image a second time through the override file:

docker compose -f docker-compose.yml -f docker-compose.monitor.yml up -d

Where the rest lives

The docs/ directory holds the long-form references. They ship with the code, so they are the version that matches the server you are running.

Deployment and running

selfhosting.md covers the whole deployment: the database, mail, the reverse proxy, the volumes, the upgrades. backups.md covers taking a backup and putting it back.

The parts of the system

architecture.md says how the API, the gateway, the client and the jobs fit together. services.md lists the services and what each one is for.

Voice and media

voice.md covers the relay, the credentials it mints, the stunkey another host needs, and the page that says whether the stack is working.

The client and the shell

desktop.md and launcher.md cover the desktop application. theming.md covers the design tokens, pages.md the editable pages, and extensions.md the extension API.

Running a community

community-settings.md covers the settings a community administrator has, and continuum.md covers federation between instances.

Checks and audits

verification-matrix.md says what is checked and how. production-readiness.md, dashboard.md and the two audit files record what was found and what was done about it.

If you find an error in this page, the support page explains where to say so.