hachiflow.com
Migration

La porte de sortie est une fonctionnalité du produit

La plupart des plateformes hébergées prouvent leur verrouillage en rendant le départ douloureux. Nous avons répété le départ jusqu'à ce qu'il fonctionne depuis le README du paquet lui-même, sur une machine vierge, sans rien connaître de nos systèmes. Notes de terrain sur la migration dans les deux sens : vers l'hébergé quand l'ops vous fatigue, vers la sortie quand vous voulez les clés.

Why chat history is usually hostage

Ask a hosted chat platform for your data and what you usually get is a records file: a zip of JSON and attachments, fine for compliance and grep, useless for continuing the conversation anywhere else. The live community, the thing you actually built, does not come out. "Export your data" and "leave with your community" are different promises, and most of the industry only makes the first one.

Buzz makes the gap concrete rather than commercial. Every message is signed by its author, and the signature covers the timestamp. A relay accepts an event only from the key that signed it, and only while the timestamp is fresh, about fifteen minutes in the stock relay. So nobody can re-post last year's history into a fresh relay over the wire: not us, not you, not the people who wrote the messages. This is not our policy to relax; it is how the protocol defends itself against forged and replayed history, and it binds everyone running Buzz anywhere.

We learned that the honest way. Our first export design assumed signed events could be republished to any relay; a drill disproved it, and we corrected our own copy. Since then the export has been a record and has said so plainly. This article is about what we built past that: a migration path that moves the live community in both directions, without pretending the wire can do what it cannot.

Identity lives on the hostname

The second fact everything rests on: a Buzz community's identity is welded to the relay hostname. When a client connects, the relay resolves which community it is serving from the host the connection arrived on, and every message, membership and file hangs off that community's internal id. The hostname is not where the community lives so much as how the community is named, and the binding exists in exactly one place: one row in the relay's database that names the community's host.

That row is the hinge the whole migration swings on, and it also produces the advice we now give everyone before anything else: choose your hostname carefully, in both directions, before people move in. Fifty members later it is the name your community answers to, and changing it is a migration, not a settings toggle.

Restore, rebind, boot

Because the wire is closed to the past, real migration runs underneath it, at the database. Three steps, and the order is the entire trick: restore, rebind, boot.

Restore first, with the relay not yet running: load the database dump into an empty database, and mirror the media into the new media store. Media in Buzz is content-addressed, named by the hash of its own bytes, so it does not care which hostname serves it. Then rebind: update that one host row to the new hostname. Then, and only then, boot the relay. On startup it looks for a community at its configured host, finds the row you rebound, and adopts it: same community, same internal id, full history, original timestamps.

Boot first and you meet the failure that makes all this look impossible: the relay finds no community at its host, helpfully seeds a fresh empty one, and your restored history sits in the same database, orphaned beside it. Everything appears healthy and every room is empty. Rebind before first boot and the relay never gets the chance to be helpful. In our drills the restored history was readable over an authenticated connection the moment the relay came up, and the same owner key that ran the old instance posted new messages on the new one.

ancien hôte auto-hébergé ou hébergé nouvel hôte hébergé ou auto-hébergé par le réseau refusé, pour tout le monde les horodatages signés expirent en minutes 1 · restaurer dump de la base + médias 2 · rattacher une ligne : l'hôte de la communauté 3 · démarrer le relay adopte la communauté rattacher avant le premier démarrage
Le réseau refuse l'historique rejoué pour tout le monde, la migration passe donc en dessous : restaurez le dump, rattachez la ligne d'hôte au nouveau nom, et démarrez seulement ensuite. Le relay adopte la communauté qu'il trouve au lieu d'en semer une vide.
restaurer, rattacher, démarrer
# relay à l'arrêt : chargez le dump, puis répliquez les médias $ pg_restore --no-owner -d <empty-db> community.dump # une ligne, avant le premier démarrage du relay $ psql -d <empty-db> \ -c "UPDATE communities SET host = 'chat.your-new-name.example'" # le relay adopte la communauté qu'il trouve à son hôte $ docker compose up -d relay

Tout le mécanisme, abrégé. L'ordre est l'astuce : un relay démarré avant le rattachement sème une communauté vide à son hostname, et votre historique restauré reste orphelin à côté.

In: when you are tired of ops

Direction one: in. You have been self-hosting Buzz, the novelty of 2 a.m. disk alerts has worn off, and the reason you have not moved is that moving usually means starting over: new accounts, empty rooms, a "the history is in the old thing" link nobody ever follows. That is the move we drilled end to end this week, with the same import tooling we will run against your community, and none of those costs survived the drill.

What carried, verified rather than hoped: the full history, with its original timestamps, because a restore is a database operation and the wire's fifteen-minute window never gets a vote. Every member identity, with zero re-enrollment, because a Buzz identity is a Nostr keypair the member holds rather than an account we issue: the same keys that worked against your hostname keep working against the new one, and nobody is asked to sign up again. And the media, byte for byte, verified against its content addresses.

What we ask from you is deliberately small: a dump of your database and a way to read your media store. We do the rest hands on, with a backup taken before the import touches anything, and the drill's checklist runs before we call it done. There is no self-serve import button today, and we are in no hurry to add one: a migration is the one day your community deserves an operator paying full attention.

Out: when you want the keys

Direction two: out. Every hosted workspace has always been able to download an export: the signed events, the media, a README. That is the record layer, and it is honest about being a record. What the bundle is gaining now is a sovereign layer next to it: the full database dump, the exact container image reference the workspace was running, a compose file, and a restore script that enforces restore, rebind, boot in that order.

The test we hold it to is the only one that counts: no help from us. In the drill, a clean machine that had never touched our systems followed the bundle's own README and came back up self-hosted on a new hostname. The same owner key signed in. The full history was there. Media served at its content addresses. And the instance accepted new messages, which is the difference between a museum exhibit and a community that moved.

An export you can only restore with the vendor on a support call is not a door, it is a leash with extra steps. So the README is the standard we drill against: leaving should be a boring afternoon of following instructions that work. We would rather earn the ops than own the exit.

The edges, plainly

A migration story without its edges is an advertisement, so here they are. First: old messages that embedded absolute URLs, thumbnails inside older uploads, upload responses that quoted a full address, keep pointing at the old hostname forever. Those URLs live inside signed content, and rewriting signed content would be forgery, so no honest migration can touch them. The media files themselves are content-addressed and serve correctly from the new host; it is only the literal URLs frozen inside old signed messages that still name the place you left.

Second, on the way out: the relay's own signing key does not come with you. Your restored instance generates a fresh relay identity, so the system notices the old relay signed stay attributed to the identity that actually signed them, which is exactly what signatures are for. Member access is unaffected: your people are their own keys, and those travel because they were never ours to hold.

Third, in both directions: pick the destination hostname like it is permanent, because it effectively is. The same weld that makes the one-row rebind sufficient makes the hostname the one thing you cannot casually change later.

Control, in both directions

Put it together and sovereignty stops being a slogan and becomes a checklist. Your people hold their own keys, so no operator, including us, sits between them and their identity. Your history restores under a hostname you control, in whichever direction you are moving. The wire refuses replayed history for everyone equally, and the database path underneath works for everyone equally. Control is not a feeling in a pricing table; it is whether the door out has been drilled until it opens. Ours has, this week, from the README alone.

Where this leaves you

The hosted side of the trade lives at hachiflow.com: $50 a month flat, your own relay at name.hachiflow.chat, and the migration described here done with you, hands on, in whichever direction you are heading.

And if you never send us a dollar, take the mechanism: restore, rebind, boot, in that order, with the rebind before the relay's first start. That one ordering is most of what a Buzz migration is. We proved it on a clean machine this week, and it is yours to keep.

← Toutes les notes