Integrating trueseal-sync
Your app starts, and before the first screen appears it needs to know whether this device is in a group, who else is in it, and whether the relay is reachable. Then it needs to hear about new messages and changes for as long as it runs. This page covers that wiring. Pairing, membership and messages each have their own page.
All three SDKs expose one object, TrueSeal, with the same operations, names, events and errors. Only the syntax differs. Swift uses async throws, Kotlin suspend functions and exceptions, and TypeScript promises. The examples here use neutral syntax.
Opening it
trueseal = await TrueSeal.open(relay, storage, namespace = "default")
relayis the Relay Address, one string with the relay’s host, ports and public key:trueseal://<relay public key hex>@host[:receivePort][?push=pushPort]. Ship it as a build-time constant. Relay Address and keypair explains where it comes from.storageis the directory for Session State. Use a directory that belongs to your app, so no other app shares its identity. On Android, build theStoragefrom your app’s context; the API reference has each platform’s spelling.namespaceselects one independent identity and group inside that directory. One namespace holds one group, so an app that needs a device in two groups opens two objects with two namespaces.maxPayloadBytesis optional. Pass a value lower than the Protocol Size Limit (61,440 bytes (60 KiB)) if your relay’s Operator set a lower Relay Size Limit.
open() returns before the relay connection is up and connects in the background. Once the device is a member, send() works whether the relay is reachable or not, and messages wait in the outbox until it is. Don’t write reconnect logic; the library retries on its own.
Open one object per namespace when the app starts and keep it for the life of the process. It survives removal, leaving and Destroy Group. After each, the library wipes the namespace, gives the device a new identity, and the same object can pair again.
open() can fail with:
| Case | What happened | What to do |
|---|---|---|
invalidRelayAddress | open() was given a Relay Address it can't parse. | Fix the configured Relay Address. Copy it as the relay CLI prints it. |
invalidNamespace | open() was given a namespace it refuses. | Pass another namespace, or leave the default. |
storage | Session State can't be read or written, or a migration failed; the old store is left intact. | Check that the storage location exists and is writable, then call TrueSeal.open again. The old store is intact. |
storeTooNew | open() found Session State written by a newer release; the store is left unchanged. | Ship the newer release again. The store is unchanged, so nothing is lost. |
Ship the Relay Address as a build-time constant, so invalidRelayAddress means fixing that constant. Upgrading covers storeTooNew.
State
These values describe the device at any moment. Each SDK makes them observable in its usual way: @Observable in Swift, StateFlow in Kotlin, and getters with change events in TypeScript.
statusis one ofnotJoined,pendingJoin,member,leavingordestroying.connectionis one ofconnecting,connected,disconnectedorrelayVersionUnsupported{min, max}.meis this device andmembersevery other member, each aMember{id, name}.
Read status and connection separately. A device can be connected with no group yet, or in a group with the relay out of reach. Neither is an error. relayVersionUnsupported is the one connection state the user can’t wait out. The relay speaks a different protocol version, and messages stay queued until your app’s library and the relay speak the same version again.
Events and handlers
trueseal.onEvent(event => …)
trueseal.onMessage(async message => …)
trueseal.onDeliveryIssue(issue => …)
onEventtakes any number of listeners. The cases arestatusChanged(status, reason),membersChanged(members),joinRequest(request),connectionChanged(connection)andadmissionDropped(name, reason). Events raised before the first listener exists are kept and delivered to it.onMessageregisters the one message handler. The library takes no message from the relay until it exists, so register it once your app can store what arrives. A second call fails withmessageHandlerAlreadySet.onDeliveryIssueis optional. It reports messages the library couldn’t deliver or handle.
Every on… call returns a handle that cancels the registration.
Sending and receiving explains what the message handler must do, and Delivery issues what each issue means.
Closing it
close() stops the connection and the handlers and keeps Session State. Every later call on that object fails with closed. Open a new object to start again.
Conflicts are your app’s to resolve
TrueSeal hands each device’s messages to your handler in the order that device sent them, and doesn’t order messages from different devices. When two devices change the same data while apart, your app decides which change wins. Common patterns:
- Append-only records. Each change is a new record, and the app shows all of them. Nothing conflicts. Fits chat, activity feeds and logs.
- Last writer wins. Put a timestamp or a counter in each message and keep the newest. Fits single values such as a setting or a location.
- CRDTs. Send the updates of a CRDT library such as Yjs or Automerge as message bodies. The library merges them in any order.
A new member starts empty
A device that joins receives only messages sent after the other members learn it joined. Nothing is replayed from before. When membersChanged shows a new member, your app decides what to send it: everything, a snapshot of current state, or nothing for data that only matters from now on, such as a live location.
It works when
- A second device appears in
memberson both devices after pairing. - A
send()on one device runs the message handler on the other, in both directions. - A message sent while the relay was unreachable arrives after it comes back.