Private MLS group messaging for iPhone, powered by the Marmot Rust runtime.
SwiftUI interface. Rust cryptographic core. Generic APNS wakes. Local-first diagnostics.
Dark Matter iOS is the native iPhone client for the Dark Matter/Marmot secure messaging stack. The app gives iOS users a polished SwiftUI chat experience while delegating accounts, MLS group state, storage, relay catch-up, message processing, encrypted media, push-token cryptography, telemetry, and audit-log plumbing to the vendored MarmotKit UniFFI package.
The project is intentionally split along the platform boundary: Swift owns presentation, navigation, lifecycle, notifications, and Apple integration; Marmot owns protocol state and durable encrypted data.
- End-to-end encrypted MLS group messaging over the Dark Matter/Marmot relay stack.
- Nostr identity flows: create/import local identities, display npubs, share profile deep links, and scan QR codes.
- Rich conversations with markdown, replies, reactions, mentions, encrypted image attachments, read state, and group system events.
- Multi-account chat lists, account switching, group management, profile publishing, and NIP-65/inbox relay editing.
- Privacy-preserving notifications: APNS carries only a generic wake, while the Notification Service Extension locally catches up Marmot state and renders visible content on device.
- Developer and forensic tooling: diagnostics, streaming debug rows, telemetry controls, local audit JSONL files, and protected transcript export.
- Extension-safe shared projection code for local notifications, notification-service rendering, localization, profile sanitization, and app-group configuration.
darkmatter-ios is a SwiftUI app wrapped around a Rust runtime:
- SwiftUI owns the app shell, onboarding, settings, chat UI, navigation, sheets, toasts, and foreground/background lifecycle.
AppStateis the observable hub for global app state. It owns the liveMarmotClient, the active account, pending navigation, visible-chat tracking, notification subscriptions, native push sync, and runtime suspend/resume.MarmotKitis generated from the sibling Dark Matter Rust repo. It exposes the Marmot runtime through UniFFI and ships here as a vendored xcframework plus generated Swift bindings.Shared/compiles into both the app and the Notification Service Extension, so files there must remain extension-safe.- The Notification Service Extension opens the shared Marmot store, runs bounded relay catch-up, and projects local notification content without putting private metadata in the APNS payload.
darkmatter-ios/- main SwiftUI app target.darkmatter-ios/Core/- app state, Marmot client setup, lifecycle, notifications, routing, telemetry, diagnostics helpers, and shared UI utilities.darkmatter-ios/Chats/,Conversation/,Group/,Settings/,Profile/,Onboarding/- feature screens and view models.NotificationServiceExtension/- APNS wake handling and local notification decoration.Shared/- extension-safe code shared by the app and notification extension.Vendored/MarmotKit/- generated UniFFI Swift bindings and the prebuilt Marmot static library xcframework.scripts/sync-bindings.sh- rebuilds and re-vendorsMarmotKitfrom the sibling Rust checkout.docs/manual-tests.md- release-focused manual checks for flows that are expensive to automate.AGENTS.md- canonical coding-agent guidance for this repo.
- Xcode with iOS 18+ SDK support.
- Apple developer signing configured for device builds, APNS, App Groups, and the Notification Service Extension.
- The sibling Dark Matter Rust repo at
../darkmatteronly when regenerating Marmot bindings. Normal Swift builds use the vendoredMarmotKitbundle.
Current identifiers:
- Main app bundle ID:
dev.ipf.darkmatter - Notification Service Extension bundle ID:
dev.ipf.darkmatter.NotificationService - App Group:
group.dev.ipf.darkmatter
List project targets and schemes:
xcodebuild -list -project darkmatter-ios.xcodeprojBuild for a simulator:
xcodebuild build \
-project darkmatter-ios.xcodeproj \
-scheme darkmatter-ios \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro'Run unit tests:
xcodebuild test \
-project darkmatter-ios.xcodeproj \
-scheme darkmatter-ios \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro'Build the device release artifact:
xcodebuild build \
-project darkmatter-ios.xcodeproj \
-scheme darkmatter-ios \
-configuration Release \
-destination 'generic/platform=iOS'If a simulator name is unavailable on your machine, list local destinations:
xcodebuild -showdestinations \
-project darkmatter-ios.xcodeproj \
-scheme darkmatter-iosVendored/MarmotKit/MARMOT_VERSION records the Dark Matter commit used for the current bindings.
Regenerate bindings after changes in marmot-uniffi or any Rust crate it depends on:
./scripts/sync-bindings.shUse DARKMATTER_DIR if the Rust checkout is not the sibling default:
DARKMATTER_DIR=/path/to/darkmatter ./scripts/sync-bindings.shDo not hand-edit generated files in Vendored/MarmotKit. Change Rust/UniFFI, regenerate, then validate the iOS app.
- APNS provider payloads stay generic. Sender names, account IDs, group IDs, message IDs, and plaintext are never sent to Apple.
- The app and Notification Service Extension share the Marmot root through the App Group container.
- Marmot stores account secrets in the Keychain.
- User defaults hold preferences such as active account, developer mode, recent reactions, and diagnostics self-check state.
- Decrypted media cache files and temporary transcript exports use complete file protection.
- Remote group-image search is an explicit third-party egress surface and uses ephemeral, no-cookie/no-cache URL sessions.
Telemetry is compiled into the vendored MarmotKit bundle with the otlp-export feature. The app reads these Xcode build settings through Info.plist:
DARKMATTER_OTLP_ENDPOINT- defaulthttps://otlp.ipf.dev/v1/metricsDARKMATTER_OTLP_BEARER_TOKEN- defaults to$(OTLP_TOKEN_DARKMATTER_IOS)DARKMATTER_TELEMETRY_ENVIRONMENT-stagingorproduction; TestFlight builds are stagingDARKMATTER_AUDIT_LOG_BEARER_TOKEN- defaults to$(AUDIT_LOG_TOKEN_DARKMATTER_IOS)
Put local secrets in Config/TelemetrySecrets.xcconfig and do not commit real tokens. Audit-log uploads use the endpoint compiled into MarmotKit and a token separate from OTLP, because the audit tracker and metrics collector are different services.
Before a TestFlight build:
- Run focused tests for the behavior you changed.
- Run a Release device build.
- Run
git diff --check. - Walk the relevant checks in
docs/manual-tests.md. - Confirm signing still includes APNS for the app and App Group access for both the app and extension.