17 KiB
SimpleX Chat iOS -- System Architecture
Technical specification for the iOS app's layered architecture, FFI bridge, event system, and extension model.
Related specs: README | API Reference | State Management | Database Related product: Product Overview
Source: SimpleXApp.swift | AppDelegate.swift | ContentView.swift | ChatModel.swift | SimpleXAPI.swift | AppAPITypes.swift | APITypes.swift | API.swift
Table of Contents
- Layered Architecture
- FFI Bridge
- Event Streaming
- Database Architecture
- App Lifecycle
- Extension Architecture
- Remote Desktop Control
1. Layered Architecture
The app follows a strict layered model where each layer communicates only with its immediate neighbor:
┌─────────────────────────────────────────┐
│ SwiftUI Views │ Rendering, user interaction
│ (ChatListView, ChatView, ComposeView) │
├─────────────────────────────────────────┤
│ ChatModel (ObservableObject) │ App state, @Published properties
│ ItemsModel, Chat, ChatTagsModel │ Per-chat state, tag filtering
├─────────────────────────────────────────┤
│ SimpleXAPI (FFI Bridge) │ chatSendCmd/chatApiSendCmd
│ AppAPITypes (ChatCommand/Response) │ JSON serialization/deserialization
├─────────────────────────────────────────┤
│ C FFI Layer │ chat_send_cmd_retry, chat_recv_msg_wait
│ (SimpleX.h, libsimplex.a) │ Compiled Haskell via GHC cross-compiler
├─────────────────────────────────────────┤
│ Haskell Core (chat_ctrl) │ Chat logic, chat protocol (x-events),
│ (Simplex.Chat.Controller) │ database operations, file management
├─────────────────────────────────────────┤
│ simplexmq library (external) │ SMP/XFTP protocols, SMP Agent,
│ (github.com/simplex-chat/simplexmq) │ double-ratchet (PQDR), transport (TLS)
└─────────────────────────────────────────┘
Key invariant: No SwiftUI view directly calls FFI functions. All communication flows through ChatModel or dedicated API functions in SimpleXAPI.swift.
Source Files
| Layer | File | Role | Line |
|---|---|---|---|
| Views | Shared/Views/ChatList/ChatListView.swift |
Chat list rendering | |
| Views | Shared/Views/Chat/ChatView.swift |
Conversation rendering | |
| State | Shared/Model/ChatModel.swift |
ChatModel, ItemsModel, Chat classes |
L337, L74, L1271 |
| API | Shared/Model/SimpleXAPI.swift |
FFI bridge functions | L93 |
| API | Shared/Model/AppAPITypes.swift |
ChatCommand, ChatResponse, ChatEvent enums |
L15, L649, L1055 |
| FFI | SimpleXChat/SimpleX.h |
C header declaring Haskell exports | |
| FFI | SimpleXChat/APITypes.swift |
APIResult<R>, ChatError, ChatCmdProtocol |
L27, L699, L17 |
| Core | ../../src/Simplex/Chat/Controller.hs |
Haskell command processor — see processCommand in Controller.hs |
2. FFI Bridge
C Functions (SimpleX.h)
The Haskell core exposes these C functions, declared in SimpleXChat/SimpleX.h:
typedef void* chat_ctrl;
// Initialize database, apply migrations, return controller
char *chat_migrate_init_key(char *path, char *key, int keepKey, char *confirm,
int backgroundMode, chat_ctrl *ctrl);
// Send command string, return JSON response string
char *chat_send_cmd_retry(chat_ctrl ctl, char *cmd, int retryNum);
// Block until next async event arrives (or timeout)
char *chat_recv_msg_wait(chat_ctrl ctl, int wait);
// Close/reopen database store
char *chat_close_store(chat_ctrl ctl);
char *chat_reopen_store(chat_ctrl ctl);
// Utility: markdown parsing, server validation, password hashing
char *chat_parse_markdown(char *str);
char *chat_parse_server(char *str);
char *chat_password_hash(char *pwd, char *salt);
// File encryption/decryption
char *chat_write_file(chat_ctrl ctl, char *path, char *data, int len);
char *chat_read_file(char *path, char *key, char *nonce);
char *chat_encrypt_file(chat_ctrl ctl, char *fromPath, char *toPath);
char *chat_decrypt_file(char *fromPath, char *key, char *nonce, char *toPath);
Swift Bridge Functions (SimpleXAPI.swift)
// Synchronous send -- blocks calling thread
func chatSendCmdSync<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true,
bgDelay: Double? = nil, ctrl: chat_ctrl? = nil) throws -> R // L91
// Async send -- dispatches to background
func chatApiSendCmd<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true,
bgDelay: Double? = nil, ctrl: chat_ctrl? = nil) async -> APIResult<R> // L215
// Low-level FFI call -- serializes command to string, calls chat_send_cmd_retry, decodes JSON
func sendSimpleXCmd<R: ChatAPIResult>(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl?,
retryNum: Int32 = 0) -> APIResult<R> // SimpleXChat/API.swift L114
Data Flow
- Swift constructs a
ChatCommandenum value (e.g.,.apiSendMessages(type:id:scope:live:ttl:composedMessages:)) ChatCommand.cmdStringserializes it to a command string (e.g.,"/_send @1 json {...}")sendSimpleXCmdpasses the string tochat_send_cmd_retryvia C FFI- Haskell core processes the command, returns JSON response string
- Swift decodes JSON into
APIResult<R>whereR: ChatAPIResult - Result is either
.result(R),.error(ChatError), or.invalid(type, json)
Background Task Protection
All FFI calls are wrapped in beginBGTask() / endBackgroundTask() to prevent iOS from killing the app mid-operation. The maxTaskDuration is 15 seconds.
3. Event Streaming
The Haskell core emits async events (new messages, connection status changes, file progress, etc.) that are not direct responses to commands. These are received via polling:
Haskell Core --[chat_recv_msg_wait]--> Swift event loop --> ChatModel update --> SwiftUI re-render
The event loop is implemented in ChatReceiver, and events are dispatched by processReceivedMsg.
Event Types (ChatEvent enum)
Key async events delivered from core to UI:
| Event | Description | Line |
|---|---|---|
newChatItems |
New messages received | L1070 |
chatItemUpdated |
Message edited by sender | L1072 |
chatItemsDeleted |
Messages deleted | L1074 |
chatItemReaction |
Reaction added/removed | L1073 |
contactConnected |
New contact connected | L1062 |
contactUpdated |
Contact profile changed | L1066 |
receivedGroupInvitation |
Group invitation received | L1077 |
groupMemberUpdated |
Group member info changed | L1067 |
callInvitation |
Incoming call | L1115 |
chatSuspended |
Core suspended (background) | L1056 |
rcvFileComplete |
File download finished | L1099 |
sndFileCompleteXFTP |
File upload finished | L1110 |
Events are decoded as ChatEvent enum in Shared/Model/AppAPITypes.swift and dispatched to update ChatModel / ItemsModel properties, triggering SwiftUI view re-renders via @Published property observation.
4. Database Architecture
Two SQLite databases in the app group container (shared with NSE):
| Database | File | Contents |
|---|---|---|
| Chat DB | simplex_v1_chat.db |
Messages, contacts, groups, profiles, files, tags, preferences |
| Agent DB | simplex_v1_agent.db |
SMP connections, keys, queues, server info |
Both databases use the DB_FILE_PREFIX = "simplex_v1" prefix. The database path is resolved via getAppDatabasePath() in SimpleXChat/FileUtils.swift, which checks dbContainerGroupDefault to determine whether to use the app group container or legacy documents directory.
See Database & Storage specification for full details.
5. App Lifecycle
Initialization Sequence (SimpleXApp.swift)
// SimpleXApp.init()
1. haskell_init() // Initialize Haskell RTS (background queue, sync)
2. UserDefaults.register(defaults:) // Register app preference defaults
3. setGroupDefaults() // Sync preferences to app group container
4. setDbContainer() // Set database path L122
5. BGManager.shared.register() // Register background task handlers
6. NtfManager.shared.registerCategories() // Register notification action categories
State Transitions
┌──────────┐
│ Launched │
└─────┬─────┘
│ initChatAndMigrate()
v
┌──────────┐
│ DB Setup │ chat_migrate_init_key()
└─────┬─────┘
│ startChat() SimpleXAPI.swift L2098
v
┌──────────┐
│ Active │ apiActivateChat() SimpleXAPI.swift L358
└─────┬─────┘
│ scenePhase == .background
v
┌──────────┐
│Background │ apiSuspendChat(timeoutMicroseconds:) SimpleXAPI.swift L368
└─────┬─────┘
│ scenePhase == .active
v
┌──────────┐
│ Active │ startChatAndActivate()
└──────────┘
Scene Phase Handling (SimpleXApp.swift)
.active: CallsstartChatAndActivate(), processes pending notification responses, refreshes chat list and call invitations.background: Records authentication timestamp, callssuspendChat()(unless CallKit call active), schedulesBGManagerbackground refresh, updates badge count.inactive: No explicit handling (transitional state)
CallKit Exception
When a CallKit call is active during backgrounding, chat suspension is deferred (CallController.shared.shouldSuspendChat = true) until the call ends, to maintain the WebRTC session.
6. Extension Architecture
Notification Service Extension (NSE)
The NSE (SimpleX NSE/NotificationService.swift) is a separate process that:
- Receives encrypted push notification payload from APNs
- Initializes its own Haskell core instance (
chat_ctrl) with shared database access - Decrypts the push payload using stored keys
- Generates a visible
UNMutableNotificationContentwith the decrypted message preview - Delivers the notification to the user
Database sharing: Both main app and NSE access the same database files in the app group container (APP_GROUP_NAME). Coordination uses file locks to prevent concurrent write conflicts.
Lifecycle: The NSE has a ~30-second execution window per notification. It must initialize Haskell RTS, open the database, decrypt, and deliver within this window.
Share Extension (SE)
The Share Extension (SimpleX SE/) allows sharing content (text, images, files) from other apps into SimpleX conversations.
7. Remote Desktop Control
Optional desktop pairing allows controlling the mobile app from a desktop client:
- Pairing: Encrypted QR code scanned by desktop client establishes a session
- Commands:
connectRemoteCtrl,findKnownRemoteCtrl,confirmRemoteCtrl,verifyRemoteCtrlSession,listRemoteCtrls,stopRemoteCtrl,deleteRemoteCtrl - State:
ChatModel.remoteCtrlSession: RemoteCtrlSession?tracks the active session - Transport: Encrypted reverse HTTP transport between mobile and desktop
- Source:
Shared/Views/RemoteAccess/ConnectDesktopView.swift, seeRemote.hsin../../src/Simplex/Chat/
Source Files
| File | Path | Line |
|---|---|---|
| App entry point | Shared/SimpleXApp.swift |
L17 |
| App delegate | Shared/AppDelegate.swift |
L15 |
| Root view | Shared/ContentView.swift |
L24 |
| FFI bridge | Shared/Model/SimpleXAPI.swift |
L93 |
| Low-level FFI | SimpleXChat/API.swift |
L115 |
| App state | Shared/Model/ChatModel.swift |
L337 |
| API types | Shared/Model/AppAPITypes.swift |
L15 |
| Shared types | SimpleXChat/APITypes.swift |
L27 |
| C header | SimpleXChat/SimpleX.h |
|
| NSE | SimpleX NSE/NotificationService.swift |
|
| Haskell core | ../../src/Simplex/Chat/Controller.hs — see processCommand in Controller.hs |
|
| Chat protocol (x-events, message envelopes) | ../../src/Simplex/Chat/Protocol.hs |
External: simplexmq Library
The lower-level protocol and encryption layers are in the separate simplexmq library:
| Component | Spec | Implementation |
|---|---|---|
| SMP protocol | simplexmq/protocol/simplex-messaging.md |
simplexmq/src/Simplex/Messaging/Protocol.hs |
| XFTP protocol | simplexmq/protocol/xftp.md |
simplexmq/src/Simplex/FileTransfer/Protocol.hs |
| SMP Agent (duplex connections) | simplexmq/protocol/agent-protocol.md |
simplexmq/src/Simplex/Messaging/Agent.hs |
| Double ratchet (PQDR) | simplexmq/protocol/pqdr.md |
simplexmq/src/Simplex/Messaging/Crypto/Ratchet.hs |
| Post-quantum KEM (sntrup761) | simplexmq/protocol/pqdr.md |
simplexmq/src/Simplex/Messaging/Crypto/SNTRUP761.hs |
| TLS transport | — | simplexmq/src/Simplex/Messaging/Transport.hs |
| File encryption | — | simplexmq/src/Simplex/Messaging/Crypto/File.hs |