‹ All AirLink docs

AirLink

Integration and building

The AirLinkCore Swift package, its main types for the mesh, sealing, chat and calls, and how to build and test the apps from source.

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.app

The package has no third-party dependencies. It uses CryptoKit, CoreBluetooth, Network, AVFoundation and AudioToolbox for Opus, SQLite and Accelerate.

Main types

Identity

TypePurpose
MeshIdentityThe device's Ed25519 and X25519 private keys and display name. card gives the public half
ContactCardPublic keys and display name. fingerprint and callID are derived from the keys
FingerprintEight bytes. broadcast is all ones. readable groups the hex in pairs of bytes
CallIDThe eight-character number. init?(typed:) parses forgiving input; matches(_:) checks a fingerprint
IdentityStoreLoads or creates the identity once, keeps it sealed on disk, and never overwrites a file it cannot read. rotate(displayName:) issues new keys

Mesh

TypePurpose
MeshPacketThe 41-byte header and payload. encoded() and decode(_:)
MessageIDSixteen random bytes
FloodRouterDuplicate table, age checks, voice and delivery rules. adaptiveTTL(peerCount:)
OutboxStore and retry for originated messages
StreamChunkerSplits packets for a link and reassembles them from any chunking
SealedMessageseal(_:from:to:messageID:) and open(_:as:messageID:)
MeshTransportThe protocol a radio implements: broadcast, send(_:to:), start, stop, nudge
BLEMeshTransport, LANMeshTransportBluetooth LE and Wi-Fi
CompositeTransportSeveral transports presented as one
MeshNodeTies it together: sealing, routing, retries, receipts, hellos

Calls

TypePurpose
CallSessionEphemeral key agreement and per-frame sealing. established(with:as:), seal(_:), open(_:)
ReplayWindow64-frame duplicate window
VoiceCodecOpus encode and decode through AudioToolbox. isAvailable checks support
VoiceMediaLinkMicrophone, speaker and the two media roads
EchoCancellerThe software NLMS canceller
CallModelThe call state machine

Chat

TypePurpose
ChatModelOwns a MeshNode over Bluetooth and Wi-Fi, the conversation history and contact names
ChatStoreThe SQLite store with per-row sealing
StoreCipherSeals 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_64

build-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.xcodeproj

project.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 AECBenchmark

The 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 devices

Options: --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.