Gryt

Overview

The Gryt server — chat, identity, permissions and everything that is not media

The Gryt server is the part that knows who people are. It holds the messages, the members, the permissions and the invites, it decides who may join, and it tells the SFU which voice rooms exist. Media never touches it.

It is a Node service: Express for HTTP, Socket.IO for the live connection, SQLite through node:sqlite for storage. One process per community.

What runs alongside it

A working server is usually three or four containers:

PieceWhy it is there
The serverChat, identity, permissions, invites, the database
The SFUVoice and video. One per machine, shared by every server on it
An image workerThumbnails and compression. One per server, because it reads that server's job queue
Object storageUploads, when STORAGE_BACKEND=s3

The image worker is per-server rather than shared for a specific reason: it pulls jobs out of the server's own SQLite database rather than being handed them over a network.

Configuration

The server reads its settings from the environment, except for the handful that live in the database and are covered further down.

The ones you will actually set

VariableDefaultWhat it does
SERVER_NAMEThe name people see
SERVER_DESCRIPTIONShown on the join preview
HOST127.0.0.1Bind address. Has to be 0.0.0.0 for anything but this machine to connect
PORT5000HTTP and WebSocket port
JWT_SECRETSigns access tokens. Required
DATA_DIR./dataWhere SQLite and local uploads go
CORS_ORIGINComma-separated origins allowed to connect
SFU_WS_HOSTWhere the server reaches the SFU, internally
SFU_PUBLIC_HOSTWhere clients should reach the SFU. Usually different
STUN_SERVERSGoogle'sPassed through to clients

HOST defaults to loopback

A server left on 127.0.0.1 still advertises itself over mDNS, so clients on the network find it and then cannot connect. The server says so in its log at startup. Set HOST=0.0.0.0 for a server other machines should reach.

Capacity and limits

VariableDefaultWhat it does
MAX_MEMBERS100How many members the server accepts
VOICE_MAX_USERS0Voice seats. 0 is no limit
GRYT_INVITE_MAX_JOINS_PER_HOURPer-invite join rate
GRYT_SERVER_ICON_MAX_MB25Server icon upload ceiling. SERVER_ICON_MAX_MB also works
MESSAGE_CACHE_TTL_MS30000How long a message page stays cached
REFRESH_TOKEN_TTL_DAYS7Refresh token lifetime

VOICE_MAX_USERS is the server's own cap. The SFU has a separate MAX_PEERS that covers every server sharing it.

Storage

VariableDefaultWhat it does
STORAGE_BACKENDs3s3 or filesystem
S3_ENDPOINTObject store endpoint
S3_BUCKETBucket name
S3_REGIONRegion
S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEYCredentials
S3_FORCE_PATH_STYLENeeded by MinIO and most self-hosted stores
DISABLE_S3Turn object storage off entirely
IMAGE_WORKER_URLWhere the image worker listens
MEDIA_SWEEP_INTERVAL_MS600000How often orphaned media is swept
MEDIA_SWEEP_GRACE_MS1800000How long an orphan is left alone first
EMOJI_QUEUE_CONCURRENCY1Emoji jobs at once, clamped to 1–4
EMOJI_QUEUE_POLL_MS750Queue poll interval, clamped to 250–5000

Object keys are flat: uploads/<uuid>.<ext>. Several servers can share one bucket safely, because nothing ever lists the bucket and every delete works from a key stored in that server's own database.

Identity

VariableDefaultWhat it does
GRYT_IDENTITY_TIERSaccountWhich identities may join. Add local to allow device-only keypairs
GRYT_OIDC_ISSUERThe account issuer to trust
GRYT_OIDC_AUDIENCEExpected audience
GRYT_TRUSTED_CERT_ISSUERSWhich certificate authorities an identity may be signed by
GRYT_SERVER_ENABLEDtrueWhether the server accepts joins at all

GRYT_AUTH_MODE was renamed

GRYT_SERVER_ENABLED used to be GRYT_AUTH_MODE, with values required and disabled. The name suggested it controlled whether members needed an account. It never did: that is GRYT_IDENTITY_TIERS, and the two do not interact. All the old setting ever did was refuse every join.

The old name and its old values still work so that upgrading does not take a server offline, and the new name wins when both are set. An unrecognised value refuses joins and says so, rather than guessing.

Behind a proxy

VariableDefaultWhat it does
GRYT_TRUSTED_PROXY_HOPS0How many proxies in front may be believed

Zero by default. x-forwarded-for is a header any client can set, so trusting it blindly lets one client walk around every per-IP limit by varying a string. Hops are counted from the right, which is the only end infrastructure writes. A server behind a proxy or a tunnel has to set this, or every client arrives wearing the proxy's address and shares one rate-limit bucket.

Discovery

VariableDefaultWhat it does
SERVER_DISCOVERABLEInitial mDNS advertising state
MDNS_INTERFACEPin advertising to one interface
EXTERNAL_HOSTThe address to advertise instead of the bound one

Invite rate limiting

VariableDefault
SERVER_INVITE_MAX_RETRIES8
SERVER_INVITE_RETRY_WINDOW_MS300000
SERVER_INVITE_IP_MAX_RETRIES
SERVER_INVITE_RETRY_COOLDOWN_MS
SERVER_INVITE_MAX_COOLDOWN_MS

The rest

SERVER_ID, SERVER_INSTANCE_ID, SERVER_PASSWORD and SERVER_VERSION identify the server to the SFU and to clients. VOICE_CHANNEL_ID, VOICE_CHANNEL_NAME and ADDITIONAL_CHANNELS seed the channel list on first boot. NODE_ENV and DISABLE_STUN behave as you would expect.

Settings that live in the database

Four settings are not environment variables. They are stored in the server's own database, they can be changed while it runs, and changing one does more than write a row:

SettingValues
joinPolicyinvite, open
discoverableon, off
lanOpenon, off
profanityModeoff, flag, censor, block

Turning discoverable off has to withdraw the mDNS advertisement, not just record the intent, so these go through an apply path rather than a direct write. Writing the row by hand leaves a server broadcasting itself while its own configuration says it is hidden.

Owners change these from a client. An operator who is not the owner changes them through the management API.

The management API

A server administrator running Gryt on their own machine is not necessarily its owner, and should not have to be to turn off LAN discovery on a box they administer. So there is a second, tiny listener for exactly that.

VariableDefaultWhat it does
GRYT_ADMIN_TOKENEnables the listener. Without it nothing starts
GRYT_ADMIN_PORT5099Port for the management listener
EndpointWhat it does
GET /management/settingsThe four settings above
PATCH /management/settingsChange some of them. Send only the keys you are changing
GET /management/health{"ok":true}

All three need Authorization: Bearer <GRYT_ADMIN_TOKEN>, compared as digests in constant time.

Two separate things protect this listener. The token covers other users and processes on the same machine. What covers everybody else is where the port is published: the Compose file the CLI generates publishes it to 127.0.0.1 only, which Docker enforces at the host before a packet reaches the container.

Do not publish this port

There is deliberately no check on the caller's address, because a container cannot see a useful one. A request from the host's own loopback arrives at a container published on 127.0.0.1 with a source address of the Docker bridge gateway. A guard on req.ip would reject every legitimate request while proving nothing, which is worse than no guard because it reads like one.

The loopback publish is the boundary. Keep it.

Requires server 1.5.0 or newer. Against anything older the endpoint is not there at all, and the gryt CLI says so.

Endpoints

EndpointWhat it does
GET /health{ status, service, serverName, timestamp }
GET /infoThe public join preview
GET /iconThe server icon, 404 when none is set
GET /metricsPrometheus

The full REST and Socket.IO surface is in the API reference.

On this page