Toj is a messaging app for iPhone with 1:1 and group chats, media, multi-device sync, and voice and video calls. It is designed for mobile networks where 100-500 kbps, high latency, packet loss and dropped connections are normal.
The app reads from and writes to an encrypted database on the phone, so the interface never waits for the server. Messages are sent in the background and retried until the server confirms them, and every device catches up from the same ordered history.
- Offline-first client. Chats, drafts and search work from local data (SQLite with SQLCipher). A sent message shows up right away and is marked delivered when the server acknowledges it.
- Idempotent sends. Each message carries a client-generated ID and the server stores one message per ID, so retrying after a lost reply is safe. After a reconnect, devices fetch what they missed in order, by sequence number.
- Encryption at rest. Message content is sealed with AES-GCM under per-record keys, which are wrapped by per-account keys. Lookups go through HMAC blind indexes, so the server can find a record without storing the value it searches by.
- Multi-device sync. History is stored on the server, synced to every signed-in device, and restored on a new login.
- Standard transport. TLS and WebSocket over HTTPS, with no custom wire protocol.
- Calls. 1:1 voice and video over WebRTC, and group calls through a LiveKit SFU with frame encryption. They are built and tested, and will be switched on once the call servers are deployed.
| Area | Result |
|---|---|
| Automated tests | 538 iOS and 455 backend tests on every pull request |
| Sync under network faults | 50,000 messages across 5 fault profiles: 0 lost, 0 duplicated, 0 mismatches between devices |
| Sync re-delivery bug | Found by the fault harness in 97 of 100 runs, 0 of 100 after the fix |
| Convergence time | 4.01 s worst-case p99 for all devices to match, with server replies dropped on purpose |
| CI | 4 jobs per pull request; every GitHub Action pinned to a full commit SHA |
Method, machine details and raw data: docs/results/sync-chaos.md.
%%{init: {"theme":"base","themeVariables":{
"primaryColor":"#111318","primaryTextColor":"#F4F5F7","primaryBorderColor":"#2A2E36",
"lineColor":"#6E7480","textColor":"#F4F5F7","fontSize":"14px",
"clusterBkg":"#08090B","clusterBorder":"#2A2E36"
}}}%%
flowchart LR
subgraph device["iPhone: SwiftUI, iOS 26"]
UI["Views<br/>Liquid Glass"]
Store[("Encrypted local store<br/>SQLite + SQLCipher")]
Sync["Sync engine<br/>outbox · retry · resume"]
Media["Media engine<br/>chunked · resumable"]
end
subgraph edge["Transport: HTTPS"]
WS["TLS + WebSocket<br/>REST /v1"]
end
subgraph backend["Backend: Bun + TypeScript"]
API["Auth · messaging · groups<br/>presence · fan-out"]
Env["Envelope encryption<br/>AES-GCM + wrapped keys"]
DB[("PostgreSQL<br/>messages · media chunks")]
end
UI <--> Store
Store <--> Sync
Sync <--> WS
Media <--> WS
WS <--> API
API --> Env --> DB
classDef n fill:#111318,stroke:#2A2E36,color:#F4F5F7
class UI,Store,Sync,Media,WS,API,Env,DB n
style device fill:#08090B,stroke:#2A2E36,color:#9096A1
style edge fill:#08090B,stroke:#2A2E36,color:#9096A1
style backend fill:#08090B,stroke:#2A2E36,color:#9096A1
sequenceDiagram
autonumber
participant A as Sender device
participant S as Server
participant B as Other devices
A->>A: Save message + outbox entry in one transaction
Note over A: UI shows the message
A->>S: send(clientMsgId)
S->>S: Idempotent insert · encrypt · sequence
S--xA: Reply lost on a bad link
A->>S: Retry send(clientMsgId)
S-->>A: Same message, stored once
S-)B: WebSocket hint
B->>S: getDifference(since)
S-->>B: Ordered updates, paged
Note over A,B: All devices end with the same history
Client (Toj/)
Swift and SwiftUI on iOS 26, using Liquid Glass. I/O, cryptography and networking run off the main thread and can be cancelled.
| Module | Responsibility |
|---|---|
App/ |
App entry point, root view, and CloudAppModel, the shared state the screens bind to |
Core/Store/ |
Encrypted local database (GRDB + SQLCipher) and an on-device FTS5 search index |
Core/Sync/ |
Outbox, multi-device sync, presence, draft sync, network monitoring |
Core/Cloud/ |
REST/WebSocket client, endpoint config, token storage, chunked media |
Core/Transport/ |
Connection lifecycle, reconnection and backoff |
Core/Calls/, Core/GroupCalls/ |
WebRTC 1:1 calls and LiveKit SFU group calls with frame encryption |
Core/Crypto/ |
libsignal engine for the planned Secret Chats |
Features/ |
Screens by feature: messaging and search, contacts, groups, profile, settings, calls, group calls |
DesignSystem/ |
Shared theme components (see the design system) |
Backend (server/)
Bun and TypeScript over PostgreSQL. It handles accounts, messaging, groups, media, presence, delivery receipts and fan-out over TLS and WebSocket.
| Area | Approach |
|---|---|
| Encryption at rest | Each record has its own data key, sealed with AES-GCM and wrapped by a per-account key. Unwrapped keys are cached briefly and zeroed after use. Retiring a key stops new unwraps first, then waits for cached copies to expire before deleting (envelope-crypto.ts) |
| Lookups | Versioned HMAC blind indexes with a separate key domain per use (phone lookup, OTP codes, tokens and others), so a digest made for one domain can't be used in another. The index key can be rotated (blind-index.ts) |
| Schema changes | Expand/contract migrations, with concurrent index builds in separate files so deploys don't lock tables (schema-*-expand.sql, -contract.sql, -concurrent.sql) |
| Sign-in | Phone number and one-time code over Telegram, WhatsApp or SMS, with fraud rules that record the reason for every block (otp-risk.ts) |
| Media storage | Media chunks are stored encrypted in PostgreSQL, which avoids running a separate object store |
More detail, including transport and calls: docs/architecture.md.
- Fault-injection testing. A Toxiproxy harness runs four headless clients against the real server under five network profiles: clean, 3G, connection resets, dropped replies and truncated replies. A shorter run is part of CI.
- Pre-registered experiments. Plans, metrics and pass thresholds are committed before the first measurement, and result tables are generated from the committed raw data. The OTP fraud evaluation was tuned on one set of seeds and then run once on held-out seeds.
- Release gates in code. The staging server refuses to start if a call feature flag is set. The flags stay off until the release gates are met.
- Pinned dependencies. The WebRTC framework is a reproducible build that CI checks by checksum and build attestation. LibSignalClient is pinned by tag and by the checksum of its prebuilt library. SwiftPM and CocoaPods versions are checked against reviewed revisions.
Default chats are encrypted in transit with TLS and at rest with AEAD envelope encryption. The server holds the keys, which is what lets history sync across devices and come back after a new login. Access to stored content is gated and logged.
Secret Chats are planned as an opt-in mode with end-to-end encryption through libsignal. The server will not be able to read them, and each one will live on a single device.
Toj uses standard cryptographic primitives and libraries only. Logs never contain message content, keys or full phone numbers. CI checks the app's privacy manifest against the privacy data map.
To report a vulnerability, follow the security policy and please don't open a public issue.
Toj is pre-launch. The app and backend are feature-complete and run on a private staging server. The remaining work is provisioning, infrastructure and testing on real mobile networks.
| Area | State |
|---|---|
| 1:1 and group messaging, media, search, multi-device sync | Running on staging |
| Phone sign-in, sessions, two-step verification | Running on staging |
| Voice and video calls, group calls | Built and tested; off until TURN servers are deployed and the release gates pass |
| Push notifications | Built; needs APNs provisioning |
| Field testing on carrier networks | Next |
| Hosting close to users | Planned for launch; the server address is one config value |
| Secret Chats | Planned; the libsignal engine is already in the app |
| Android | Planned |
Channels, bots, payments and server-side search come after launch.
| Requirement | Version |
|---|---|
| macOS with Xcode | 26.5+ (iOS 26 simulator runtime) |
| CocoaPods | recent |
| Bun | 1.3.11 |
| PostgreSQL | 17.x |
git clone https://github.com/mmarufov/Toj.git
cd Toj
scripts/fetch-webrtc-xcframework.sh # downloads the pinned WebRTC build and verifies it
pod install
open Toj.xcworkspace # open the workspace (CocoaPods)Scheme Toj runs the app against a local backend and runs TojTests.
Scheme Toj Staging runs it against staging. See iOS staging.
cd server
bun install
createdb toj_dev && bun run migrate
PORT=8787 bun run server.ts # simulators reach it on 127.0.0.1# iOS
xcodebuild -workspace Toj.xcworkspace -scheme Toj \
-destination 'platform=iOS Simulator,OS=26.5,name=iPhone 17 Pro' test
# Backend: real PostgreSQL, one file at a time (the suites share a database)
cd server
createdb toj_test
export DATABASE_URL=postgres://localhost:5432/toj_test
bun run migrate
find . -path ./node_modules -prune -o -name '*.test.ts' -print | sort \
| while read -r f; do bun test --timeout 15000 "$f" || break; doneSteps to reproduce the fault-injection run are in docs/results/sync-chaos.md.
Toj/ iOS client (Swift, SwiftUI)
TojTests/, TojUITests/ iOS unit and UI tests
TojBroadcastExtension/ ReplayKit screen-share extension
server/ Backend (Bun, TypeScript, PostgreSQL)
server/chaos/ Network fault-injection harness
infra/coturn/ TURN relay deployment template
scripts/ Build, verification and codegen scripts
docs/ Documentation, results, and the tojchat.tech site
| Document | What it covers |
|---|---|
| Architecture | Data model, transport, encryption at rest, calls, CI |
| Test results | Pre-registered experiments with committed raw data |
| Design system | Color, typography, spacing, motion, Liquid Glass rules |
| Privacy data map | Data categories, checked in CI against the privacy manifest |
| Backend staging · Operations | Deployment runbook, retention and security conventions |
| TURN relay | coturn deployment and capacity gates |
| Release records | Call release evidence and gates |
Full index: docs/.
Please read CONTRIBUTING.md before opening an issue or pull request, and the security policy before reporting anything security-related.
Source-available: published for inspection and audit, all rights reserved. See LICENSE. Third-party components keep their own licenses, listed in NOTICE.md.
