Shoal Messages and Shoal Push: data flow and data map

Draft. This document has not completed legal review and is not yet in force.

DRAFT, 30 September 2026. Engineering description of what the hosted service processes, where, and for how long, as deployed. It is the factual basis for the privacy policy draft and for legal review; it is not itself legal advice. Every retention value here is set in the deployment configuration.

Our trust model distinguishes five things. They are the colour classes in the diagram and the sections below.

  1. Matrix-native E2EE: conversations between Matrix users in encrypted rooms. Content is encrypted on the phone; we cannot read it.
  2. Bridged conversations: Signal and Telegram chats. The bridge is a participant in the external protocol and necessarily handles plaintext.
  3. Retained metadata: what the homeserver, bridges and push server keep to function (who, which room, when), even when content is encrypted.
  4. Bridge credentials and sessions: what lets the bridge act as the subscriber on Signal or Telegram.
  5. Operational logs: service logs, metrics and backups.

Diagram

flowchart LR
  classDef e2ee fill:#d8f3dc,stroke:#2d6a4f,color:#081c15
  classDef bridged fill:#ffe5d9,stroke:#9d0208,color:#370617
  classDef meta fill:#e0e1dd,stroke:#415a77,color:#0d1b2a
  classDef cred fill:#fff3b0,stroke:#9c6644,color:#3d2b1f
  classDef ops fill:#e7c6ff,stroke:#5a189a,color:#240046

  subgraph Phone["Subscriber's phone (Sailfish OS)"]
    App["Shoal Messages<br/>local store encrypted with a key from Sailfish Secrets"]
    Dist["Shoal Push distributor"]
  end

  subgraph Cell["Shoal cell (EU VPS)"]
    Caddy["Caddy (TLS termination, no access log)"]
    HS["Synapse homeserver"]
    HSDB[("Synapse DB:<br/>accounts, devices, room state,<br/>encrypted events, pushers, IPs 3 d")]
    Media[("Media store<br/>30 d after last access")]
    SB["mautrix-signal"]
    TB["mautrix-telegram"]
    BDB[("Bridge DBs:<br/>remote sessions and keys,<br/>contacts, id mappings")]
    Push["ntfy (push)"]
    PDB[("ntfy DB:<br/>user, token, ACL,<br/>undelivered pushes 12 h")]
    Lic["Licence service<br/>(licence id, plan, expiry)"]
    Logs["Logs: warnings/errors only,<br/>3 x 10 MB ring per service"]
  end

  Backup[("Encrypted backups<br/>7 days, EU storage")]
  Signal["Signal servers"]
  Telegram["Telegram servers"]
  Fed["Other Matrix homeservers"]
  MoR["Merchant of record<br/>(payment data)"]

  App -->|"E2EE Matrix events (ciphertext)"| Caddy --> HS
  HS --> HSDB
  HS --> Media
  HS -->|"E2EE events to/from other servers"| Fed
  App -->|"end-to-bridge encrypted events"| HS
  HS <-->|"appservice API"| SB
  HS <-->|"appservice API"| TB
  SB -->|"plaintext in memory,<br/>Signal protocol E2EE on the wire"| Signal
  TB -->|"plaintext in memory,<br/>MTProto to Telegram (cloud chats)"| Telegram
  SB --> BDB
  TB --> BDB
  HS -->|"push: room id, event id, counts only"| Push
  Push --> PDB
  Push -->|"token-authenticated stream"| Dist --> App
  MoR -->|"webhook: licence id, plan, paid-until"| Lic
  App -->|"licence id (token refresh)"| Lic
  Cell -.->|"nightly, encrypted to an offline key"| Backup

  class App,Fed e2ee
  class SB,TB,Signal,Telegram bridged
  class HSDB,Media,PDB,Lic meta
  class BDB cred
  class Logs,Backup ops

Diagram source (Mermaid). The site shows diagrams as text so it needs no scripts.

1. Matrix-native end-to-end encrypted conversations

2. Bridged conversations (Signal, Telegram)

3. Retained metadata

DataWhereRetention
Account: Matrix id (random by default), password hash, display name, licence id linkSynapseUntil deprovisioning; erased on deactivation
Devices, access tokens, E2EE public keysSynapseUntil logout; unused devices after 90 days
Room membership and state (room names, members, including bridged contacts' display names and avatars)SynapseLife of the room; state is not purged by message retention
Event metadata (sender, room, timestamp, size, type) and ciphertextSynapse30 days default (room policy 1 to 90 days)
Media (encrypted in E2EE rooms; avatars in clear)Synapse media store30 days after last access; remote 7 days
Client IP and user agent per deviceSynapse3 days
Pushers (pushkey = the subscriber's ntfy topic URL)SynapseUntil the device removes it
Push messages (room id, event id, unread count)ntfyDelivered immediately; undelivered held 12 hours
ntfy user, token, topic prefixntfyUntil deprovisioning
Licence id, app, plan, expiry, merchant subscription and transaction referencesLicence serviceLife of the licence plus accounting retention with counsel
Payment and billing identityMerchant of record (not us)Merchant's policy

4. Bridge credentials and sessions

BridgeWhat is heldVisible to the subscriber asHow it ends
SignalLinked-device identity keys, sessions, pre-keys, profile keys, group state, recipient records (Signal ids, phone numbers)"Shoal Messages (hosted bridge)" under Signal Settings, Linked devicesUnlink in Signal; logout from the bridge; deprovisioning; Signal unlinks devices after prolonged inactivity
TelegramAuthorised session (auth key), cached access hashes, usernames and phone numbers of contacts seen"Shoal Messages (hosted bridge)" under Telegram Settings, DevicesTerminate the session in Telegram; logout; deprovisioning

5. Operational logs, metrics and backups

Subprocessors and third parties (draft list)

PartyRoleData
VPS provider (Hetzner, Germany/Finland)Hosting, backup storageEverything above, encrypted backups
Signal Messenger LLC (US)The subscriber's own Signal serviceWhat any Signal client sends
Telegram (Telegram FZ-LLC / Telegram Messenger Inc.) entityThe subscriber's own Telegram serviceWhat any Telegram client sends
Merchant of record (Paddle or Lemon Squeezy, undecided)Payment, VATBilling identity, payment data
Other Matrix homeserversFederation, when the subscriber joins their roomsRoom events for those rooms

Source: services/privacy/data-flow.md in the Shipwright repository; paths and ADR numbers in the text refer to it.