Migration from RocksDB to PostgreSQL

Your question

I’ve attempted a migration from RocksDB to PostgreSQL using the import/export feature of Stalwart (v0.16) as explained in the documentation. The import seemed to work, i.e., if I \dt postgres shows me all the tables

psql -U stalwart -d stalwart -h localhost -W
Password: 
psql (16.13 (Ubuntu 16.13-0ubuntu0.24.04.1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

stalwart=> \dt
          List of relations
 Schema |  Name   | Type  |  Owner   
--------+---------+-------+----------
 public | a       | table | stalwart
 public | b       | table | stalwart
 public | d       | table | stalwart
 public | e       | table | stalwart
 public | f       | table | stalwart
 public | g       | table | stalwart
 public | h       | table | stalwart
 public | i       | table | stalwart
 public | j       | table | stalwart
 public | k       | table | stalwart
 public | l       | table | stalwart
 public | m       | table | stalwart
 public | n       | table | stalwart
 public | o       | table | stalwart
 public | p       | table | stalwart
 public | q       | table | stalwart
 public | r       | table | stalwart
 public | s       | table | stalwart
 public | s_cal   | table | stalwart
 public | s_card  | table | stalwart
 public | s_email | table | stalwart
 public | s_trace | table | stalwart
 public | t       | table | stalwart
 public | u       | table | stalwart
 public | w       | table | stalwart
 public | x       | table | stalwart
 public | y       | table | stalwart
(27 rows)

However, when I start stalwart I everything in the WebUI seems empty, and also the listeners are set to the default ones.

It shouldn’t be a configuration problem, as stalwart seems to correctly import the database to postgres. But here is my config.json:

{
    "@type":"PostgreSql",
    "host":"127.0.0.1",
    "port":5432,
    "database":"stalwart",
    "authUsername":"stalwart",
    "authSecret":{"@type":"Value", "secret":"mysecret"}
}

I didn’t find any error in the log.

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

The root cause is clear. Your config.json only tells Stalwart where the database is, not what’s in it. The WebUI appearing empty (with default listeners, etc.) is a sign that Stalwart found the PostgreSQL database empty on first start and ran through its bootstrap/setup wizard, writing fresh default settings into it – overwriting (or not finding) your migrated data.

Here are the most likely reasons and how to verify:

1. The import wrote to a different database than Stalwart is now reading from

Double-check that the --config you passed to --import points to exactly the same config.json that Stalwart uses at runtime, and that both point to the same PostgreSQL host/database/user. Your config.json shows host: 127.0.0.1 – make sure the import was also run on the same machine (not a remote host) with that same file.

2. Stalwart re-bootstrapped because the database appeared empty

As documented, when Stalwart starts and finds no configuration in its data store it enters bootstrap mode, which sets up fresh default settings. If the import failed silently (or wrote to a different DB), Stalwart would have just bootstrapped a clean instance into your PostgreSQL database. Check whether the PostgreSQL tables actually contain rows:

SELECT COUNT(*) FROM s;  -- settings table

If it returns 0, the import data didn’t land in that database.

3. Verify the import command used the correct config

The import must be run like:

$ /opt/stalwart/bin/stalwart --config /path/to/your/postgres-config.json --import /path/to/export-directory

…while Stalwart is stopped, and the config.json must reference the PostgreSQL backend (as yours does). If you used the old RocksDB config.json by mistake, the data was imported back into RocksDB, not PostgreSQL.

4. Check the export directory

Make sure the export directory is non-empty and contains the binary dump files before importing:

ls -lh /path/to/export-directory/

The docs are clear that the import reads all subspace families from that directory, so if the directory was empty or pointed to the wrong path, the import would appear to succeed but do nothing.

Reference: Database Migration docs

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

Tables present but empty means that the registry rows didn’t end up in the database the running server uses.

First, did you set EXPORT_TYPES during --export? The default (unset) means “everything”; anything else restricts the dump to the listed subspace families. The registry family covers SUBSPACE_REGISTRY, SUBSPACE_REGISTRY_IDX, SUBSPACE_REGISTRY_PK and SUBSPACE_DIRECTORY; if EXPORT_TYPES was set to just blob or data, the registry rows weren’t in the dump, which is exactly the symptom you describe.

Second, is the config.json you pass to --import byte-identical to the one the running service (systemd unit / Docker container) uses at startup? --import writes to whatever storage.data and storage.blob the passed config resolves to (crates/common/src/manager/restore.rs:23-48). A mismatch silently lands the rows in a different database than the one the server connects to.

Quick check: psql -c 'SELECT COUNT(*) FROM r' on the database the running server uses. Zero rows = no registry was imported there. Non-zero = the server’s actually connected to a different database.

One more thing worth confirming, was the source RocksDB store on 0.16 (a stalwart --export produced the dump), not a copy of /var/lib/stalwart/data? --import is for 0.16 → 0.16, not for raw RocksDB directories.