{"version":1,"relay_protocol_version":1,"generated_at":"2026-08-01T14:32:44.977729+00:00","server_version":"PrivacyPortal Messenger Relay 1.0.0","code_repository":"https://github.com/privacyportal/privacyportal-messenger","code_revision":"unknown","is_canonical":false,"relay_metadata":{"operator_name":"","operator_contact":"","operator_jurisdiction":"","uptime_pledge":"","operator_added_features":[],"subpoena_policy_url":null},"alternate_endpoints":{"clearnet":"https://messenger.privacyportal.co.uk","onion":"http://m5xgtzz4hlqnpldg2zcnbplwmpgsjrzbueutxjv6qeqizskvtbcqvkad.onion"},"data_we_store":[{"name":"users","fields":["user_id (random UUID string)","account_number (16-char server-issued id from a 32-symbol alphabet; independent from cryptographic keys)","account_number_history (optional array, capped at 5: past numbers with valid_until ~7 days for lookup rerouting after rotation)","box_pub (Curve25519/X25519 public key, base64)","mlkem_public_key (optional ML-KEM-768 public key for hybrid PQ encapsulation, ~1184 bytes raw, base64)","sign_pub (Ed25519 verify key, base64)","username (optional lowercase public handle; may be null)","discoverable_by_username (boolean — whether this user appears in @username lookups via GET /identity/by-username)","username_changed_at (optional; enforces at most one username change per 24h via POST /account/username)","display_name (optional opaque nickname chosen by client)","created_at","last_seen","supported_envelope_versions (optional client capability integers, e.g. 4 = per-message ratchet)"],"retention":"Until DELETE /api/account (or manual DB wipe). Usernames are freed when cleared or when the user row is deleted.","purpose":"Public identity directory and signed-challenge lookups for account operations. Account number is independent from cryptographic identity; rotating it swaps the lookup mapping — existing conversations keep working via box_pub."},{"name":"messages","fields":["message_id (random UUID)","to_box_pub (recipient routing key)","created_at","received_at (relay enqueue time; BSON Date for Mongo TTL)","expires_at (optional; Mongo TTL index when set)","delivered / delivered_at (offline queue bookkeeping until ack/TTL)","kind (opaque client label, e.g. text | image)","ttl_seconds (optional duplicate of client intent)","meta_ciphertext (optional opaque blob)","Legacy rows: from_box_pub, envelope { ephemeral_pub, nonce ciphertext }","Sealed-sender rows: sealed=true, blob_b64 (opaque outer ciphertext; no sender field)"],"retention":"Deleted on first successful delivery acknowledgement (POST /api/messages/ack or optional WebSocket type=delivered), OR 7 days from receipt (relay enqueue time in received_at) for offline-only fallback, whichever first; also removed when expires_at TTL fires (disappearing messages), or when deleted with the recipient account.","purpose":"1:1 ciphertext routing and short-lived offline queue."},{"name":"group_messages","fields":["message_id","group_id","from_box_pub","to_box_pub (per-recipient fan-out copy)","nonce","ciphertext","kind","meta_ciphertext","ttl_seconds","created_at","received_at (relay enqueue time; BSON Date for Mongo TTL)","expires_at (Mongo TTL when set)"],"retention":"Deleted on recipient acknowledgement (same paths as direct messages), OR 7 days from received_at, whichever first; plus expires_at TTL and account deletion as applicable.","purpose":"Store encrypted group payloads per recipient for offline delivery."},{"name":"groups","fields":["group_id","owner_box_pub","members (list of member box_pub)","wrapped_keys (NaCl box payloads — opaque to server)","name_ciphertext (optional opaque)","created_at"],"retention":"Until every member leaves or accounts are deleted (group row deleted when empty).","purpose":"Client-side group key distribution metadata; member list is visible to the relay as public keys only."},{"name":"files","fields":["file_id","owner_box_pub","size","data (raw ciphertext bytes)","created_at","expires_at (Mongo TTL; default ~30 days when client omits TTL)"],"retention":"Until TTL deletes the row or manual/admin intervention.","purpose":"Temporary ciphertext blob hosting for attachments referenced inside E2EE messages."},{"name":"username_nonces","fields":["box_pub","purpose","nonce","expires_at (TTL ~5 minutes)"],"retention":"Mongo TTL on expires_at (single-use signed challenges).","purpose":"Replay-resistant proof-of-possession for registration, username changes, account delete, account-number rotation, optional username set/clear, and ML-KEM key publication."},{"name":"spent_tokens","fields":["_id (32-byte Privacy Pass token_input, opaque)","spent_at","expires_at (Mongo TTL ~48 hours)"],"retention":"Replay ledger only; TTL deletes documents automatically after ~48 hours.","purpose":"Ensure each anonymous sealed-send token is used at most once."},{"name":"pp_issue_rl","fields":["box_pub","last_issue_at","expires_at (Mongo TTL ~24 hours rolling)"],"retention":"Ephemeral rate-limit ledger only (cooldown enforcement); not used to reconstruct issuance payloads.","purpose":"Limit Privacy Pass batch issuance frequency per registered identity."},{"name":"guest_rooms","fields":["room_id (server-generated alphanumeric)","created_at","last_activity_at","participants (ephemeral X25519 box_pub base64, optional ML-KEM public base64, HMAC proof base64, optional display_name ≤32 chars, joined_at, last_seen_at)","expires_at (Mongo TTL)"],"retention":"Up to 1 hour after creation, or when empty shortened to ~5 minutes after the last participant leaves (whichever applies); TTL deletes the row automatically.","purpose":"Ephemeral invite-only chat rooms with no accounts. The relay never receives the shared URL secret (clients keep it in the URL fragment only). HMAC proves announcement possession of that secret."},{"name":"guest_messages","fields":["room_id","message_id","sender_eph_pub","recipient_eph_pub","ciphertext (opaque base64)","sent_at (BSON Date)","delivered (boolean; row deleted on recipient WS ack)"],"retention":"Deleted when the recipient acknowledges delivery over the guest WebSocket, or after ~1 hour via Mongo TTL on sent_at.","purpose":"Short-lived ciphertext queue for guest rooms when the recipient is briefly offline."},{"name":"relay_metrics","fields":["_id (messages_delivered_YYYY-MM-DD, privacy_pass_tokens_YYYY-MM-DD, or client_usage_YYYY-MM-DD)","delivered (integer count of queue rows removed after recipient acknowledgement; older historical rows may contain legacy successful live-socket push counts)","issued (integer aggregate count of Privacy Pass tokens evaluated)","sessions.android|desktop|web|unknown (authenticated socket sessions/reconnects; integer aggregates)","confirmed_receipts.android|desktop|web|unknown (messages removed after a recipient acknowledgement; integer aggregates)","download_requests.android|desktop|web|unknown (accepted APK/EXE download requests; integer aggregates, not completed installs)","expires_at (client_usage rows only; Mongo TTL at approximately 90 days)"],"retention":"client_usage rows expire automatically after approximately 90 days. Legacy messages_delivered and Privacy Pass aggregate rows are operational counters that the operator may prune.","purpose":"Daily privacy-safe aggregate totals for confirmed message acknowledgements (with older legacy push-count history), Privacy Pass evaluation, coarse client-platform sessions/reconnects, confirmed message receipts, and accepted download requests. These counters contain no account/public keys, device identifiers, IP addresses, User-Agent strings, or per-user histories, and must not be interpreted as unique users, devices, installs, or completed downloads."},{"name":"voprf_issuer_key","fields":["NIST P-384 private key PEM on disk (`backend/.voprf_secret_key.pem`, gitignored, chmod 0600 on Unix)"],"retention":"Until the operator rotates or deletes the PEM file.","purpose":"RFC 9497 VOPRF (P384-SHA384) issuer secret — proves authenticity of anonymous send-tokens. Compromise enables forged tokens (spam), not decryption of ciphertext."}],"data_we_never_store":["Plaintext message bodies","Plaintext file contents","Sender identity for sealed-send message rows (no from_box_pub stored for sealed transport)","Private keys or recovery phrases","ML-KEM secret keys never leave the user's device — they are derived from the user's seed phrase locally.","Per-user IP address persistence","Geolocation","Third-party advertising or analytics identifiers","Per-user or per-device platform, build-version, session, receipt, or download histories; client-platform metrics are daily aggregate integers only","Any mapping between Privacy Pass tokens issued in a batch and the box_pub that requested issuance after the HTTP response completes"],"relay_without_storage":["Typing indicator JSON may be forwarded between online WebSocket peers (not written to MongoDB).","Read receipt JSON may be forwarded between online WebSocket peers (not written to MongoDB)."],"client_features":["Per-message ratchet (v=4 envelope) — message keys advance per-message and are deleted after the user's chosen retention window.","Server stores no ratchet state; forward secrecy is entirely client-side.","Optional hardware-backed unlock (StrongBox / Android Keystore) — encryption key wrapping is delegated to the device's secure element where available."],"client_side_ai":{"summary":"All AI features in the official PrivacyPortal Messenger client run on-device using bundled ONNX Runtime (WebAssembly). No message content, embeddings, vectors, or derived metadata are transmitted to the relay or to any third-party model API.","bundled_models":[{"name":"gte-tiny.onnx","purpose":"Sentence embeddings for offline semantic search","approx_on_disk_mb":22}]},"client_app":{"reproducible_build":true,"build_instructions_url":"https://github.com/privacyportal/privacyportal-messenger/blob/main/APK_BUILD.md","release_cert_sha256":"UNSET — see repository SIGNATURES.md; operators may set PP_RELEASE_CERT_SHA256"},"logs":{"what_is_logged":["Per-minute aggregate counters only (HTTP requests counted, successful websocket push deliveries, inbound websocket JSON messages, active websocket connections snapshot).","Aggregate Privacy Pass issuance totals (integer batch sizes / daily counters — never blinded element contents or tokens).","Daily coarse client-platform aggregate counters (sessions/reconnects, confirmed receipts, and accepted download requests) stored as relay_metrics integers, never per-event records.","Validation failures as integer counts of error items/types (no field values).","Sparse operational warnings with exception class names only (no client payloads, IPs, or keys)."],"what_is_NEVER_logged":["box_pub, sign_pub, username, account_number, message_id, group_id, raw IP addresses, request bodies, query strings","Privacy Pass pp_token payloads, blinded VOPRF elements, or finalized token bytes","Client platform or build version tied to a box_pub, IP address, User-Agent, or persistent device identifier","Per-connection identifiers","Mongo document contents"],"log_retention_days":7,"note":"Rotating stdout/docker logs are controlled by your host; typical deployments aggregate ~7 days."},"third_parties":["Your hosting provider (e.g. AWS Lightsail) sees TLS connections and volume timing.","Let's Encrypt or your CA sees the public hostname for certificate issuance.","Tor Project HTTPS endpoint (check.torproject.org) is contacted only by PrivacyPortal clients that run the in-app Tor checker — the relay does not call it."],"no_third_parties":["No analytics SDK on this relay","No crash reporter on this relay","No CDN terminating TLS for our API in the reference deployment","No advertising network"],"subpoena_response_capability":"Operator-dependent: with valid legal process and access to live MongoDB backups, the server may produce ciphertext blobs and routing metadata retained within TTL windows (queued direct/group rows kept at most ~7 days unless acknowledged sooner; encrypted files; group member public keys and wrapped key blobs). The relay cannot derive plaintext from ciphertext. Sealed-send rows lack sender identity at rest.","code_audit_invitation":"Source is published at https://github.com/privacyportal/privacyportal-messenger. Independent audits welcome — contact: security@privacyportal.co.uk"}