Status
AirLink has no published developer SDK. The engine is an ordinary Swift package, AirLinkCore, in the AirLink repository, and the apps are built on it. This page describes that package as it stands, for anyone building from source. If you want to use offline device-to-device messaging in your own product, tell us.
Repository layout
Package.swift Swift 6.0 tools, macOS 14 and iOS 17 platforms
Sources/AirLinkCore/ The engine: radio, mesh, crypto, chat, calls, finder maths
Mesh/ Packet, router, outbox, sealing, identity, transports
Live/ Chat store and model, call model, voice media, echo canceller
Lab/ Lab mode, its uplink and the Mac collector
Sources/AirLinkApp/ The macOS app
Sources/AirLinkCLI/ The airlink command-line finder
Tests/AirLinkCoreTests/ Unit tests
iOS app/ XcodeGen project for iPhone and iPad
scripts/build-app.sh Builds, signs and packages AirLink.appThe package has no third-party dependencies. It uses CryptoKit, CoreBluetooth, Network, AVFoundation and AudioToolbox for Opus, SQLite and Accelerate.
Main types
Identity
| Type | Purpose |
|---|---|
MeshIdentity | The device's Ed25519 and X25519 private keys and display name. card gives the public half |
ContactCard | Public keys and display name. fingerprint and callID are derived from the keys |
Fingerprint | Eight bytes. broadcast is all ones. readable groups the hex in pairs of bytes |
CallID | The eight-character number. init?(typed:) parses forgiving input; matches(_:) checks a fingerprint |
IdentityStore | Loads or creates the identity once, keeps it sealed on disk, and never overwrites a file it cannot read. rotate(displayName:) issues new keys |
Mesh
| Type | Purpose |
|---|---|
MeshPacket | The 41-byte header and payload. encoded() and decode(_:) |
MessageID | Sixteen random bytes |
FloodRouter | Duplicate table, age checks, voice and delivery rules. adaptiveTTL(peerCount:) |
Outbox | Store and retry for originated messages |
StreamChunker | Splits packets for a link and reassembles them from any chunking |
SealedMessage | seal(_:from:to:messageID:) and open(_:as:messageID:) |
MeshTransport | The protocol a radio implements: broadcast, send(_:to:), start, stop, nudge |
BLEMeshTransport, LANMeshTransport | Bluetooth LE and Wi-Fi |
CompositeTransport | Several transports presented as one |
MeshNode | Ties it together: sealing, routing, retries, receipts, hellos |
Calls
| Type | Purpose |
|---|---|
CallSession | Ephemeral key agreement and per-frame sealing. established(with:as:), seal(_:), open(_:) |
ReplayWindow | 64-frame duplicate window |
VoiceCodec | Opus encode and decode through AudioToolbox. isAvailable checks support |
VoiceMediaLink | Microphone, speaker and the two media roads |
EchoCanceller | The software NLMS canceller |
CallModel | The call state machine |
Chat
| Type | Purpose |
|---|---|
ChatModel | Owns a MeshNode over Bluetooth and Wi-Fi, the conversation history and contact names |
ChatStore | The SQLite store with per-row sealing |
StoreCipher | Seals and opens bytes with the Keychain-held key |
Using the mesh directly
import AirLinkCore
let identity = IdentityStore().identity(displayName: "Ama")
let transport = CompositeTransport([BLEMeshTransport(), LANMeshTransport()])
let node = MeshNode(identity: identity, transport: transport)
node.onContactsChanged = { contacts in
// People met through hellos. Callbacks arrive on the main queue.
}
node.onReceive = { received in
if received.content == .text {
let text = String(decoding: received.payload, as: UTF8.self)
print("\(received.sender.callID): \(text)")
}
}
node.onDelivered = { id in
// A sealed receipt for message `id` came back.
}
node.start()
// Later, to a contact from onContactsChanged:
let id = try node.sendText("Meet at the north gate", to: contact)send(_:as:to:queued:) sends any content type. Pass queued: false for traffic that is meaningless if it cannot be delivered now, as call signalling does.
The higher-level ChatModel(displayName:) builds this stack for you, persists conversations and exposes send(_:to:), contacts, messages(with:) and peerCount. CallModel(chat:) adds calls on top.
A custom transport
Any link that can carry bytes can join the mesh by implementing MeshTransport and reporting didConnect, didDisconnect and didReceive to its delegate. Packets are self-framing, so a transport does not need its own framing. The unit tests use an in-memory transport this way to prove multi-hop relaying.
Building from source
Requirements
- macOS 14 or later with Swift 6.0 tools.
- Xcode 26 and XcodeGen for the iPhone and iPad app.
- A code signing identity. macOS ties the Bluetooth permission to the app's signature, so an ad hoc build is asked again after every rebuild.
The package and the Mac app
swift build && swift test
./scripts/build-app.sh # builds, signs and packages dist/AirLink.app
UNIVERSAL=1 ./scripts/build-app.sh # arm64 and x86_64build-app.sh signs with a Developer ID if one is available, then an Apple Development certificate, then ad hoc. The airlink command-line tool is built into the bundle at AirLink.app/Contents/MacOS/airlink.
The iPhone and iPad app
cd "iOS app"
xcodegen generate
open AirLink.xcodeprojproject.yml is the source of truth; the Xcode project is generated. The app depends on AirLinkCore as a local package at the repository root, so it compiles the same engine files as the Mac app. It targets iOS 18 and runs on iPhone and iPad from one binary. Set your own development team before building for a device.
The iOS Simulator has no Bluetooth radio and no Taptic Engine. Everything that involves the radio needs real devices.
Tests
swift test runs 188 tests. The echo canceller benchmark runs only when AIRLINK_BENCH is set to an output path, and writes its convergence curve as CSV:
AIRLINK_BENCH=/tmp/aec.csv swift test --filter AECBenchmarkThe command line
The tool lives inside the app bundle at AirLink.app/Contents/MacOS/airlink.
airlink # survey every Bluetooth device in range
airlink iphone # hunt a device by name
airlink --list # paired devicesOptions: --sound, --redact, --all, --power <profile>, --for <seconds>, --csv and --help. The terminal running it needs its own Bluetooth permission.
Lab collector
For measurement sessions, the Mac app's Lab section runs a collector that devices in lab mode find over Bonjour and stream their events to. It is not needed for normal use.