Skip to content

Storage and backups

The server keeps all its data as plain files in the database directory you pass to use. There’s no separate database server. This page lists what’s in the directory and how to back it up safely.

Path Contents
config.json Your settings. Only the values you changed.
.env Secrets such as ROTUR_TOKEN.
users.json Every account, keyed by internal user ID. Local account passwords are stored as bcrypt hashes.
roles.json Roles, keyed by name.
channels.json The ordered channel list, including separators.
channels/<channel>/ Messages, in numbered JSONL files (0000.jsonl, 0001.jsonl, …) of up to 1,000 messages each.
threads/, threads_index.json Thread details and thread messages.
users/<id>/ Each account’s unread state and mention history.
polls/ One file per poll.
attachments/ Attachment details (<id>.json) and, with local storage, the files themselves.
attachment_ids.txt, attachment_stats.json Attachment index and totals.
avatars/ Local account avatars.
serverAssets/ Server icon and banner uploads.
serverEmojis/, serverEmojis.json Custom emoji images and their details.
serverStickers/, serverStickers.json Sticker images and their details.
plugins/<name>/ Installed plugins: plugin.json, main.js, data.json (the plugin’s stored data), and an optional files/ directory.
access.json Access mode, whitelist, server password hash, applications, and invites.
reports.json Reports.
webhooks.json Webhooks and their tokens.
modlog.jsonl The audit log, one entry per line.
rate_limits.json Active timeouts, so they survive a restart.
welcome.json Accounts that have already been welcomed.
mentions-version.json Marks the mention history migration as done.
backup/ Created empty by init. The server never writes to it.

You may also see these working files:

Path Meaning
*.pending next to a message file Recent edits not yet merged into the message file. Part of your data. Back it up with its message file.
.message-index/ inside a channel or thread directory A search index for fast paging. The server can rebuild it. See Rebuilding message indexes.
write-transaction.json and *.undo Present only while a multi-file write is in progress. See Interrupted writes.
*.migrated Old files kept after a migration.

Attachment files live outside this directory if you set attachments.local_dir or use S3 storage.

A complete backup has:

  • The whole database directory, including every .pending file.
  • The attachments.local_dir directory, if you set one.
  • The S3 bucket, or a versioned snapshot of it, if you use S3.

Back up the whole directory, not individual files. The files refer to each other. A copy of the messages without the thread index, polls, or attachment details is a broken server.

.message-index/ directories are optional in a backup. Leave them out and the server rebuilds them on first use.

.env contains live secrets. Store it separately from backups you share or keep off-site.

The simplest consistent backup is taken while the server is stopped:

Terminal window
# Stop the server first (Ctrl+C, or stop your service).
tar -czf ~/backups/originchats-$(date +%Y%m%d-%H%M).tar.gz \
--exclude='.message-index' \
-C ~/originchats-osl db
# Start the server again.

Keep backups outside the database directory, so each backup doesn’t include the previous ones.

A copy taken while the server runs can catch a file halfway through a change, or one file changed and a related file not yet changed. If you can’t stop the server, use a filesystem snapshot (LVM, ZFS, or Btrfs) so every file is captured at the same instant.

Restore into a new directory first, not over the live one:

Terminal window
mkdir ~/restore-test
tar -xzf ~/backups/originchats-20260901-0300.tar.gz -C ~/restore-test
./originchats-osl use ~/restore-test/db

Then check startup logs, channels, old and new messages, threads, polls, unreads, attachments, roles, webhooks, reports, and the audit log. A backup isn’t proven until a restore like this works.

  • Most files are rewritten by writing a temporary file and renaming it over the original. A crash leaves either the old or the new version, never half of one. This needs the temporary file and the original on the same filesystem, which they are unless you move files around by hand.
  • A new message is saved before anyone is told about it.
  • Changes that touch several files at once use a transaction journal. See the next section.
  • Failed saves are rolled back in memory as well, so the running server never shows something that isn’t on disk.

For a change that touches several files, the server first writes write-transaction.json. This journal lists each file’s original size, and a .undo copy of each file it will replace. The server deletes the journal once every file is written.

If the process stops in the middle, the journal is still there at the next start. Before loading anything, the server uses it to put every file back the way it was, then deletes the journal and the .undo copies.

If that recovery fails, the server logs unable to recover interrupted storage transaction and exits. Check disk space and file permissions in the database directory, then start again. Don’t delete write-transaction.json to get past the error: that would leave a change half applied. If recovery keeps failing, restore from a backup.

Each channel and thread directory can hold a .message-index/ directory. It lets the server jump to any page of history without reading every earlier message. It is built the first time history is read after startup, which can take a while on a large channel, and kept up to date after that.

The message files are the source of truth, and the index can always be rebuilt. Rebuild it after you edit, import, or replace message files by hand, or if you suspect it’s damaged:

  1. Stop the server.
  2. Delete the channel’s or thread’s .message-index/ directory.
  3. Start the server. The index is rebuilt the next time that history is read.

See persistent message indexes for how the index works.

Which client and device each member is using is kept in memory only, for as long as they’re connected. It isn’t written to users.json or to messages, and a restart clears it until members log in again.

The bot flag on an account is stored in users.json. It defaults to false, and it doesn’t grant any permissions.

The server can read, and in some cases migrate, older storage layouts, including data from the legacy Python server. See legacy formats before moving a database from another server, and keep the original backup.