# SimpleX Chat iOS -- System Architecture > Technical specification for the iOS app's layered architecture, FFI bridge, event system, and extension model. > > Related specs: [README](README.md) | [API Reference](api.md) | [State Management](state.md) | [Database](database.md) > Related product: [Product Overview](../product/README.md) **Source:** [`SimpleXApp.swift`](../Shared/SimpleXApp.swift#L1-L183) | [`AppDelegate.swift`](../Shared/AppDelegate.swift#L1-L209) | [`ContentView.swift`](../Shared/ContentView.swift#L1-L513) | [`ChatModel.swift`](../Shared/Model/ChatModel.swift#L1-L1373) | [`SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift#L1-L2915) | [`AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift#L1-L2357) | [`APITypes.swift`](../SimpleXChat/APITypes.swift#L1-L1071) | [`API.swift`](../SimpleXChat/API.swift#L1-L388) --- ## Table of Contents 1. [Layered Architecture](#1-layered-architecture) 2. [FFI Bridge](#2-ffi-bridge) 3. [Event Streaming](#3-event-streaming) 4. [Database Architecture](#4-database-architecture) 5. [App Lifecycle](#5-app-lifecycle) 6. [Extension Architecture](#6-extension-architecture) 7. [Remote Desktop Control](#7-remote-desktop-control) --- ## [1. Layered Architecture](../Shared/SimpleXApp.swift#L17-L184) 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`](../Shared/Views/ChatList/ChatListView.swift) | Chat list rendering | | | Views | [`Shared/Views/Chat/ChatView.swift`](../Shared/Views/Chat/ChatView.swift) | Conversation rendering | | | State | [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L337) | `ChatModel`, `ItemsModel`, `Chat` classes | L337, L74, L1271 | | API | [`Shared/Model/SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift#L93) | FFI bridge functions | L93 | | API | [`Shared/Model/AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift#L15) | `ChatCommand`, `ChatResponse`, `ChatEvent` enums | L15, L649, L1055 | | FFI | [`SimpleXChat/SimpleX.h`](../SimpleXChat/SimpleX.h#L1-L49) | C header declaring Haskell exports | | | FFI | [`SimpleXChat/APITypes.swift`](../SimpleXChat/APITypes.swift#L27) | `APIResult`, `ChatError`, `ChatCmdProtocol` | L27, L699, L17 | | Core | `../../src/Simplex/Chat/Controller.hs` | Haskell command processor — see `processCommand` in `Controller.hs` | | --- ## [2. FFI Bridge](../SimpleXChat/SimpleX.h#L1-L49) ### [C Functions (SimpleX.h)](../SimpleXChat/SimpleX.h#L1-L49) The Haskell core exposes these C functions, declared in `SimpleXChat/SimpleX.h`: ```c 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)](../Shared/Model/SimpleXAPI.swift#L93-L221) ```swift // Synchronous send -- blocks calling thread func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil) throws -> R // L91 // Async send -- dispatches to background func chatApiSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil) async -> APIResult // L215 // Low-level FFI call -- serializes command to string, calls chat_send_cmd_retry, decodes JSON func sendSimpleXCmd(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl?, retryNum: Int32 = 0) -> APIResult // SimpleXChat/API.swift L114 ``` ### Data Flow 1. Swift constructs a `ChatCommand` enum value (e.g., `.apiSendMessages(type:id:scope:live:ttl:composedMessages:)`) 2. [`ChatCommand.cmdString`](../Shared/Model/AppAPITypes.swift#L15) serializes it to a command string (e.g., `"/_send @1 json {...}"`) 3. [`sendSimpleXCmd`](../SimpleXChat/API.swift#L115) passes the string to `chat_send_cmd_retry` via C FFI 4. Haskell core processes the command, returns JSON response string 5. Swift decodes JSON into [`APIResult`](../SimpleXChat/APITypes.swift#L27) where `R: ChatAPIResult` 6. Result is either `.result(R)`, `.error(ChatError)`, or `.invalid(type, json)` ### [Background Task Protection](../Shared/Model/SimpleXAPI.swift#L54-L79) All FFI calls are wrapped in [`beginBGTask()`](../Shared/Model/SimpleXAPI.swift#L54) / `endBackgroundTask()` to prevent iOS from killing the app mid-operation. The `maxTaskDuration` is 15 seconds. --- ## [3. Event Streaming](../Shared/Model/SimpleXAPI.swift#L2220-L2916) 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`](../Shared/Model/SimpleXAPI.swift#L2220-L2263), and events are dispatched by [`processReceivedMsg`](../Shared/Model/SimpleXAPI.swift#L2266). ### [Event Types (ChatEvent enum)](../Shared/Model/AppAPITypes.swift#L1055-L1129) Key async events delivered from core to UI: | Event | Description | Line | |-------|-------------|------| | `newChatItems` | New messages received | [L1070](../Shared/Model/AppAPITypes.swift#L1070) | | `chatItemUpdated` | Message edited by sender | [L1072](../Shared/Model/AppAPITypes.swift#L1072) | | `chatItemsDeleted` | Messages deleted | [L1074](../Shared/Model/AppAPITypes.swift#L1074) | | `chatItemReaction` | Reaction added/removed | [L1073](../Shared/Model/AppAPITypes.swift#L1073) | | `contactConnected` | New contact connected | [L1062](../Shared/Model/AppAPITypes.swift#L1062) | | `contactUpdated` | Contact profile changed | [L1066](../Shared/Model/AppAPITypes.swift#L1066) | | `receivedGroupInvitation` | Group invitation received | [L1077](../Shared/Model/AppAPITypes.swift#L1077) | | `groupMemberUpdated` | Group member info changed | [L1067](../Shared/Model/AppAPITypes.swift#L1067) | | `callInvitation` | Incoming call | [L1115](../Shared/Model/AppAPITypes.swift#L1115) | | `chatSuspended` | Core suspended (background) | [L1056](../Shared/Model/AppAPITypes.swift#L1056) | | `rcvFileComplete` | File download finished | [L1099](../Shared/Model/AppAPITypes.swift#L1099) | | `sndFileCompleteXFTP` | File upload finished | [L1110](../Shared/Model/AppAPITypes.swift#L1110) | Events are decoded as [`ChatEvent`](../Shared/Model/AppAPITypes.swift#L1055) 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](../SimpleXChat/FileUtils.swift#L70-L294) 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()`](../SimpleXChat/FileUtils.swift#L70) in `SimpleXChat/FileUtils.swift`, which checks `dbContainerGroupDefault` to determine whether to use the app group container or legacy documents directory. See [Database & Storage specification](database.md) for full details. --- ## [5. App Lifecycle](../Shared/SimpleXApp.swift#L17-L184) ### [Initialization Sequence (SimpleXApp.swift)](../Shared/SimpleXApp.swift#L17-L38) ```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)](../Shared/SimpleXApp.swift#L38-L123) - **`.active`**: Calls `startChatAndActivate()`, processes pending notification responses, refreshes chat list and call invitations - **`.background`**: Records authentication timestamp, calls `suspendChat()` (unless CallKit call active), schedules `BGManager` background 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](../SimpleX%20NSE/NotificationService.swift#L1-L1228) ### [Notification Service Extension (NSE)](../SimpleX%20NSE/NotificationService.swift#L1-L1228) The NSE ([`SimpleX NSE/NotificationService.swift`](../SimpleX%20NSE/NotificationService.swift#L1-L1228)) is a separate process that: 1. Receives encrypted push notification payload from APNs 2. Initializes its own Haskell core instance (`chat_ctrl`) with shared database access 3. Decrypts the push payload using stored keys 4. Generates a visible `UNMutableNotificationContent` with the decrypted message preview 5. 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](../Shared/Views/RemoteAccess/ConnectDesktopView.swift#L1-L545) 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`](../Shared/Model/SimpleXAPI.swift#L1613), [`findKnownRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1620), [`confirmRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1624), [`verifyRemoteCtrlSession`](../Shared/Model/SimpleXAPI.swift#L1630), [`listRemoteCtrls`](../Shared/Model/SimpleXAPI.swift#L1636), [`stopRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1642), [`deleteRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1646) - **State**: [`ChatModel.remoteCtrlSession`](../Shared/Model/ChatModel.swift#L395)`: RemoteCtrlSession?` tracks the active session - **Transport**: Encrypted reverse HTTP transport between mobile and desktop - **Source**: [`Shared/Views/RemoteAccess/ConnectDesktopView.swift`](../Shared/Views/RemoteAccess/ConnectDesktopView.swift#L1-L545), see `Remote.hs` in `../../src/Simplex/Chat/` --- ## 8. Chat Relay Management ### Overview Chat relays are SMP servers that forward messages to channel subscribers. They are configured in the Network & Servers settings and selected during channel creation. ### Data Model | Type | Location | Description | |------|----------|-------------| | `UserChatRelay` | `ChatTypes.swift` | Relay server config: chatRelayId, address, name, domains, preset, tested, enabled, deleted | | `UserOperatorServers.chatRelays` | `AppAPITypes.swift` | Array of `UserChatRelay` per operator | | `UserServersWarning` | `AppAPITypes.swift` | Enum with `.noChatRelays(user:)` case | | `ServerSettings.serverWarnings` | `ChatListView.swift` | `[UserServersWarning]` field on `ServerSettings` struct (exposed via `SaveableSettings.servers`) | ### Relay Management Views | View | File | Description | |------|------|-------------| | `ChatRelayView` | `ChatRelayView.swift` | Edit/view relay: name, address, test, enable toggle, delete | | `ChatRelayViewLink` | `ChatRelayView.swift` | NavigationLink row showing relay status icon + display name | | `NewChatRelayView` | `ChatRelayView.swift` | Form to add new relay (name + address + test + enable toggle) | | `ServersWarningView` | `NetworkAndServers.swift` | Orange exclamation triangle + warning text | ### Key Functions | Function | File | Description | |----------|------|-------------| | `addChatRelay(...)` | `ChatRelayView.swift` | Validates name/address, appends to `userServers[nil operator].chatRelays`, calls `validateServers_` | | `deleteChatRelay(...)` | `ProtocolServersView.swift` | Marks relay as deleted or removes if no `chatRelayId` | | `validRelayName(_:)` | `ChatRelayView.swift` | Non-empty + valid display name check | | `validRelayAddress(_:)` | `ChatRelayView.swift` | Parses via `parseSimpleXMarkdown`, validates `.simplexLink(_, .relay, _, _)` | | `showRelayTestStatus(relay:)` | `ChatRelayView.swift` | ViewBuilder returning checkmark/multiply/clear icons | | `validateServers_` | `NetworkAndServers.swift` | Extended signature: now accepts optional `Binding<[UserServersWarning]>?`; calls `validateServers` which returns `([UserServersError], [UserServersWarning])` tuple | | `globalServersWarning(_:)` | `NetworkAndServers.swift` | Extracts `.noChatRelays` warning text for display | | `bindingForChatRelays(_:_:)` | `NetworkAndServers.swift` | Creates binding for `chatRelays` at operator index | ### Relay Sections in Settings "Chat relays" sections appear in: - `OperatorView`: lists relays for the operator, with header and footer - `YourServersView` (in `ProtocolServersView`): lists relays for non-operator servers, with delete support and "Add server" -> "Chat relay" option ### serverWarnings Plumbing `Binding<[UserServersWarning]>` is threaded through: `NetworkAndServers` -> `OperatorView` -> `ProtocolServersView` -> `ProtocolServerView` / `NewServerView` / `ScanProtocolServer`. All `validateServers_` calls pass the warnings binding. --- ## Source Files | File | Path | Line | |------|------|------| | App entry point | [`Shared/SimpleXApp.swift`](../Shared/SimpleXApp.swift#L17) | L17 | | App delegate | [`Shared/AppDelegate.swift`](../Shared/AppDelegate.swift#L15) | L15 | | Root view | [`Shared/ContentView.swift`](../Shared/ContentView.swift#L24) | L24 | | FFI bridge | [`Shared/Model/SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift#L93) | L93 | | Low-level FFI | [`SimpleXChat/API.swift`](../SimpleXChat/API.swift#L115) | L115 | | App state | [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L337) | L337 | | API types | [`Shared/Model/AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift#L15) | L15 | | Shared types | [`SimpleXChat/APITypes.swift`](../SimpleXChat/APITypes.swift#L27) | L27 | | C header | [`SimpleXChat/SimpleX.h`](../SimpleXChat/SimpleX.h#L1-L49) | | | NSE | [`SimpleX NSE/NotificationService.swift`](../SimpleX%20NSE/NotificationService.swift#L1-L1228) | | | 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](https://github.com/simplex-chat/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` |