Skip to main content

Storage and media

Uploaded files lie encrypted at rest in the data volume, and the node converts images and videos with its own media pipeline.

Stored files are encrypted at rest (TAVRN_STORAGE_ENCRYPTION=on, the default): attachments, avatars, headers, room icons, custom emojis and soundboard clips lie in data/blobs as AES-256-GCM ciphertext, each file with its own key, and those keys are wrapped with the node’s storage key keys/storage.key (32 random bytes, mode 0600, created at the first start) and kept in the database (blob_keys). The node decrypts while streaming a download, so the blob directory can be copied, synced or later moved to object storage without exposing content. This protects the storage layer, it is not end-to-end encryption (the node holds the keys; attachments of encrypted conversations are ciphertext anyway and are simply wrapped once more).

Back up keys/storage.key together with the database and the volume: without it every stored file is unreadable. The node refuses to start when the file is missing or does not match the stored keys, instead of creating a new one. Files written by older versions are encrypted in place in the background after the start (resumable, about 32 MB/s, progress in the log). TAVRN_STORAGE_ENCRYPTION=off stores new files unencrypted (for debugging only; encrypted files stay readable, a warning is logged at every start).

The storage key that wraps every file’s key can be replaced in seconds, because the files themselves are not re-encrypted.

Stop the node, run the rotation, start it again and back up the new key file:

Terminal window
docker compose stop tavrn
docker compose run --rm tavrn tavrn-node rotate-storage-key
docker compose start tavrn

The rotation creates keys/storage.key.next, re-wraps every file key with it (the files themselves are not re-encrypted, so it takes seconds) and then replaces keys/storage.key. An interrupted run is finished by running it again; a node started in between reads files wrapped with either key. Because the files are not re-encrypted, an old key together with an old database backup still opens the files of that time. The command prints the old and new key ids and the number of re-wrapped file keys, and warns about files wrapped with a key the node does not have (those are unreadable). With several instances stop all of them first.

Avatars, profile headers, room icons and video backgrounds are converted by ffmpeg. The image contains a minimal static ffmpeg (about 7 MB, only image and short-video decoders and the WebP encoder, no network protocols). Uploads are limited to 20 MB and 30 seconds and converted without network access with a 30-second limit per job, TAVRN_MEDIA_WORKERS (default 1) at a time; animated input becomes animated WebP with a still first frame. Results are at most 1 MB per variant for avatars and icons and 2 MB for headers. TAVRN_MEDIA_FFMPEG=off turns the conversion off (static PNG/JPEG/WebP only); outside the image point TAVRN_MEDIA_FFMPEG at an ffmpeg with libwebp (just ffmpeg-install).

Upload size, types, the executable rule, per-room quotas and attachment retention are listed under Rooms, uploads and storage and Entitlement defaults. The hourly housekeeping deletes expired attachments (messages keep listing them as “file expired”; pinned messages keep theirs), orphaned files and cached copies of other nodes’ emojis and clips (TAVRN_MEDIA_CACHE_UNUSED_DAYS). Files channels on the node’s storage have their own quota (TAVRN_FILES_CHANNELS_MAX_BYTES per channel; channels on an external store are never limited by it, see below); they do not count towards the room’s attachment quota and are exempt from attachment and message retention. They can use external WebDAV/S3 stores only with TAVRN_FILES_EXTERNAL=on (Files channels on external stores).

Every file falls into one upload category: images, audio, video, documents, archives or other (protocol: PROTOCOL.md §7a, permissions.md “Upload categories”). The node classifies plaintext uploads by their content first, then by the file name, then by the declared type; in encrypted conversations and encrypted files channels the node only sees ciphertext, so the client declares the category and the node checks the declaration (a modified client could lie; the protocol documents this).

  • Rooms decide per role which categories its members may upload (the role’s “Allowed file types”; @everyone allows all six by default, the highest role with its own setting wins), and channel or category overrides allow or deny single categories on top (“File types” in the permissions of a channel or category), evaluated like permissions. Members see what they may attach before they pick a file; a refused upload gets 403 upload.type_forbidden with the category.
  • DMs and group DMs hosted on this node use TAVRN_UPLOAD_DM_CATEGORIES (default all six).
  • Executables (programs, scripts, installers, disk images, shortcuts, macro documents) count as other and are refused on every node by default. Only TAVRN_UPLOAD_ALLOW_EXECUTABLES=true and a member who may upload other lets them through; no room setting can allow them on its own. Where they are allowed, the clients still warn before they download a file that looks like a program.
  • TAVRN_UPLOAD_ALLOWED_TYPES additionally limits the declared MIME types of attachments in unencrypted conversations; files channels and encrypted uploads are not affected by it.

New files channels are end-to-end encrypted by default (protocol: FILES.md §8); the room’s managers choose this when they create a channel and cannot change it later. In an encrypted files channel the node (and an external WebDAV/S3 store) only ever holds what the clients encrypted: the file contents and client-made thumbnails as ciphertext, and per entry an encrypted “entry record” with the name, type and key. In plaintext it keeps the entry ids, the folder tree (which entry is in which folder), who uploaded what and when, and the ciphertext sizes; never a name, a folder name, a type or content. Quotas, the entry limit, the orphan sweep, deletes and the audit log work on those fields (the audit log names entry ids instead of file names). Thumbnails of encrypted channels are always kept in the node’s own storage, also for channels on an external store. The node cannot search, preview or classify encrypted files; upload categories are declared by the clients. Members who lost their keys can still list and delete their own uploads.

Unencrypted files channels work as before: their files use the storage encryption at rest (readable by the node) or, on an external store, encrypt_files (which encrypted channels do not need and refuse).

With TAVRN_FILES_EXTERNAL=on (default off, protocol: FILES.md §5) members with Manage channels and Manage files can keep a files channel on a WebDAV server or an S3-compatible bucket instead of the node’s own storage. The node proxies uploads and downloads as streams and keeps the folder tree in its database; it never lists the store.

  • Credentials (URL, user name and password or access keys) are stored sealed with keys/secrets.key, are write-only in the API and never appear in logs or the audit log (files.backend records kind, host and encryption). Without secrets.key the node cannot reach the stores any more: back it up with the other keys.
  • encrypt_files wraps new files with the storage key before they reach the store (unreadable outside tavrn and lost without keys/storage.key); without it the store keeps the files as they are. End-to-end encrypted files channels store the clients’ ciphertext either way and refuse the switch.
  • Network rules: https only, every address the store’s name resolves to must be public, redirects are never followed, 10 s connect and 60 s read timeouts. TAVRN_FILES_EXTERNAL_PRIVATE=on also allows private, loopback and link-local addresses and plain http (stores in your LAN); this turns the SSRF protection off for them.
  • Switching it off: with TAVRN_FILES_EXTERNAL=off existing external channels answer 403 files.external_disabled until it is on again; nothing is deleted, and the queue of pending deletes on the stores (below) pauses.
  • A channel can move to another store only while it is empty; credentials and encrypt_files can change at any time. Backend changes and connection tests are limited to 20 per user and 10 minutes.
  • Limits: TAVRN_FILES_CHANNELS_MAX_BYTES (and plans and grants of files_channels.max_bytes) covers only the node’s own storage; it never limits a channel on an external store, since the node does not keep those bytes. Such a channel has its own optional limit, “Limit on the store” in its storage settings (max_bytes, empty or 0 = unlimited), set by whoever may configure the store; the node enforces it on the uploads it relays (413 files.quota_exceeded) and the clients show it as the channel’s limit.

External stores only. TAVRN_FILES_INTERNAL=off (default on; needs TAVRN_FILES_EXTERNAL=on, otherwise the node does not start and check-config names the problem) keeps the files of files channels off the node: large shared folders live on the members’ own WebDAV or S3 stores, attachments in chats stay on the node as before.

  • A new files channel is created without a store. Until its managers choose a WebDAV or S3 store in the channel settings (“Storage”), uploads and new folders answer 403 files.internal_disabled; the clients show “choose a store first” instead of the upload button. The node’s own storage cannot be chosen (403 files.internal_disabled).

  • Clients see the policy before the first upload: files_internal: false in GET /api/v1/node and policies.files_internal in the discovery document.

  • The quota: TAVRN_FILES_CHANNELS_MAX_BYTES does not limit the external stores (see “Limits” above), so it can stay as it is for the channels that still keep files on the node.

  • Exceptions per owner: the entitlement files_channels.internal (a room key, node default TAVRN_FILES_INTERNAL) decides per room. A plan or per-user override of the room’s owner with "files_channels.internal": 1 (or a grant of the room itself) allows the node’s own storage in that owner’s rooms while the node default is off, e.g. for a paid plan; the quota files_channels.max_bytes applies there as usual. The other direction works too: 0 sends an owner’s rooms to external stores on a node that keeps files. A grant that does not name the key leaves the node default; among several active grants that name it, 1 wins. A member’s own plan does not count in somebody else’s room. Clients read the room’s value as internal_allowed in the channel’s files usage and offer “This node” in the storage settings only where it is true; GET /api/v1/node keeps files_internal as the node default. When the grant ends, the room behaves like any channel of a node switched off (below): the stored files stay, new uploads are refused.

    Terminal window
    curl -sX POST https://chat.example/api/v1/admin/entitlements/grants -H "Authorization: Bearer $TOKEN" \
    -H 'Content-Type: application/json' \
    -d '{"subject":"user:alice","overrides":{"files_channels.internal":1},"note":"node storage for files channels"}'
  • Switching an existing node: channels that already keep files on the node keep them (listing, downloads, renames, moves and deletes work), but take no new uploads; once such a channel is empty its managers can move it to an external store. Thumbnails of encrypted channels (at most 1 MiB each) stay on the node, as for every external channel.

Deleting a files channel or a room deletes the files in the node’s storage at once. Objects on an external store are deleted in the background with the credentials stored for the channel; when the store is unreachable they wait in a queue (file_pending_deletes, the configuration sealed with secrets.key) that the hourly housekeeping retries for 7 days before it gives up and logs the object key. While TAVRN_FILES_EXTERNAL=off the queue pauses. On request (keep_external, offered by the client only for unencrypted external channels) the objects stay on the store; the audit log records how many were kept.

TAVRN_RETENTION_DAYS (default 0 = keep forever, at most 36500) deletes messages this many days after they were sent: the hourly housekeeping removes them from channels, DMs and group DMs hosted on the node, encrypted or not, together with their attachments (rows and files), reactions, pins and search entries; threads count their messages again. Retention never deletes pinned messages (unpin a message to let it expire) and the entries of a room’s welcome channel (the room’s security log), together with the security notices in channels (see Moderation and auto-moderation). A room owner or a member with “Manage room” can set a shorter retention for one room in the room settings (message_retention_days, 1–36500, 0 = the node’s value); a longer room value has no effect, a room can only shorten. Each run that deleted something writes one entry retention.messages to the node’s audit log (deleted, node_days, rooms with an own shorter value, incomplete when the run’s time budget of one minute ran out; the rest follows an hour later). Clients are not told: an open client keeps showing what it already loaded until it reloads, and messages users saved themselves (exports, the local search index of encrypted conversations) are outside the node’s reach. tavrn-node check-config refuses values above 36500. Tell your users in the privacy policy.

What users see, so you can answer their questions: users edit their profile under Settings → Profile: display name, avatar and profile header (PNG, JPEG, GIF, WebP or a short MP4/WebM; the node crops and converts them, animated ones loop for up to ten seconds and only move while someone hovers them or while the user speaks in voice), a short Markdown bio and up to ten linked accounts (Steam, Battle.net, Xbox, PlayStation, Nintendo, Twitch, YouTube, X, Mastodon, a website; shown as “not verified”). A click on a name or avatar opens the profile card. The footer card sets the status (online, idle, do not disturb, invisible), a custom status and an activity; the client reports idle after ten minutes without input. Settings → Privacy & safety sets who can message you (friends, friends and shared rooms, everyone), hides your activity and lists the people you blocked with your private notes. Messages from people who are not your friends wait under Message requests until you accept them; friend requests can carry a short message. The ”+” in the room list also joins rooms from an invite link or code.

For operators: TAVRN_DM_POLICY is the upper bound of what users may allow (friends, everyone_shared, everyone; default everyone). Presence is kept per node: clients push it to every node they use, nodes never forward it to each other.

Profiles of users of other nodes are copies (mirrors) kept by this node, images included, so clients never load anything from other nodes. When a user changes the public profile (name, avatar, header, bio, linked accounts, DM setting; never the private voice background), the home node tells the nodes that mirror it (profile.changed, FEDERATION.md §5.2): the nodes the user signed in on and the home nodes of friends and DM contacts. The message leaves about 5 to 15 seconds after the first change, one per user and node however many edits follow, and the other node refreshes its copy in the background (at most 10 refreshes per user and 10 minutes; beyond that it answers 429 and the outbox retries). Nodes of older versions ignore the message and refresh on the user’s next federated login there or after 24 hours. Nothing needs to be configured.

Every conversation has a whiteboard: the 🖍 button in the header of a channel, DM or group DM opens it full screen (Excalidraw: text, shapes, freehand, images). The one who opens it presents it; the others see a live dot on the button and join on demand (“Join whiteboard session”), watch live with named cursors and can leave again. Only the presenter edits until they open the board for everyone, grant single participants (“Who can edit”) or lock it again. The presenter sets a background color or image, keeps a personal library of scenes (save, load, rename, delete; the current scene is saved every 30 seconds), and closes the board for everyone. Anyone in the session can save the board as PNG or SVG or share it as an image into a channel or DM. Phones pan and zoom the board with the usual gestures; the board’s actions sit in the ”⋯” menu there. In a voice call the 🖍 control presents a board on your tile (instead of a screen share; one at a time): the others see a badge and a live preview on your tile (and a badge in the channel list), join from the tile (or by double-clicking it) and get it as a large tile they can focus, maximize or pop out into its own window. Changes travel end-to-end encrypted like messages and are never stored on the node; scenes are stored encrypted on the user’s home node (WHITEBOARD.md). Operators limit the library with TAVRN_WHITEBOARD_MAX_SCENES (default 50 scenes of at most 5 MB per user).


This page is generated from tavrn-node/docs/HANDBOOK.md (AGPL-3.0-only) when the website is built; its links were adapted for this site.