Skip to content

Engineering

How Lanai is built.

Lanai is a single SwiftUI multiplatform target with about eighty percent of its code shared across iPhone, iPad, and Mac. Persistence is SwiftData. The AT Protocol SDK sits behind a small facade so it can be replaced without anyone above noticing. All on-device AI lives in one package; only that package may import the frameworks that do the inference. The rest of the app uses a public interface and a mock.

This page is the posture, not the implementation. The shape of the work is enough; the recipes belong somewhere else.

The principles we don’t budge on.

Six commitments that show up in every package, every release, and every decision worth arguing about.

  1. Apple-native, by choice.

    SwiftUI, SwiftData, AVFoundation, Vision, NaturalLanguage, the Apple Intelligence Foundation Models, Liquid Glass. A single multiplatform target builds for iPhone, iPad, and Mac. Not Catalyst. Not a web wrapper. The platform is the partner.

  2. One codebase, three platforms.

    Around 80% of the code is identical across iPhone, iPad, and Mac. What differs is the chrome — bottom tab bar on iPhone, three-column NavigationSplitView on iPad and Mac, Reeder-style J/K keyboard nav on the desktop, a real Mac Settings window. Platform differences live in a single adapter package; everything else is universal.

  3. The SDK is swappable.

    Lanai talks to the AT Protocol through a small facade. Feature code never imports the SDK directly; it imports a value-typed model layer that could be backed by a different AT Protocol library tomorrow. The choice of SDK is an implementation detail. The choice to abstract it away is the architecture.

  4. On-device intelligence sits behind a seam.

    Every AI-touching capability lives in one package. Only that package is allowed to import the on-device frameworks. Other packages use a small public interface. The seam is where the privacy posture is enforced — not in documentation, in the import graph.

  5. Cleanup is read-layer, never write-layer.

    When Lanai cleans up the display of a post — curly quotes, real em dashes, generous leading, optical alignment — the cleanup happens at the moment of rendering. The post on the network is unchanged. Long-press any post in the app to see the original, character for character.

  6. Accessibility is the design baseline, not the accommodation.

    The largest Dynamic Type size, AX5, is the size every layout is composed at first. The default-size layout is the derivative. Body text targets APCA Lc≥75 with Lc≥90 on high-contrast surfaces. Reduce Motion is honoured at every animation site, not as a fallback but as a real path through the same design.

The stack, briefly.

The shorthand version. Each row names what's there and why, without naming what's coming next.

UI
SwiftUI with @Observable models. No ObservableObject, no @StateObject. Modern observation across the board.
Persistence
SwiftData. Offline-first. Your reading history and saves are local, encrypted, and never leave the device.
AT Protocol
Facaded behind a value-typed model layer. The wire SDK is replaceable; the rest of the app would not need to know.
Auth (v1)
App Password, stored in your device's Keychain, keyed by your DID rather than your handle. (Handles change. DIDs don't.)
Auth (v2)
OAuth via ATOAuthKit plus ASWebAuthenticationSession, when the protocol's OAuth story stabilises. The plan is sketched; the SDK lives behind the same facade.
Live updates
Pull-to-refresh in v1.0. Opt-in Jetstream live updates in v1.x, after the WebSocket lifecycle and battery work earn their own focus.
Image export
On-device rendering pipeline. Smart focus analysis (face detection → attention saliency → 9-position alignment) and content classification decide per image how to compose the crop. The post-as-image render uses the same typography pipeline as Reading Mode.
On-device AI
Vision, NaturalLanguage, CoreML, Apple Intelligence Foundation Models. All inside one package. Every other package uses a small public interface to it. No cloud round-trip. No Private Cloud Compute, by choice.
Motion
Spring physics tuned to feel like settling. Transitions 300–600ms with smooth easing. `prefers-reduced-motion` honoured everywhere, with non-motion paths that aren't just "the animation cut".
Theming
Token-driven design system. Themes are colour swaps over a fixed token graph. The High Contrast surface meets APCA Lc≥90 across body text.

Six packages, one app.

Lanai is composed of local Swift packages, each with its own imports, its own tests, and its own rules. The boundaries between them are the places the architecture lives.

  1. BlueskyKit

    The facade. Auth, fetching, posting. Wraps the upstream AT Protocol SDK. Returns value-typed models from a sibling package so feature code never touches the SDK directly.

  2. BlueskyModels

    Pure value types — Post, Profile, Feed, ThreadGate, MuteList. No UI, no SDK imports. Could ship as its own library.

  3. BlueskyPersistence

    SwiftData stack and offline cache. The single source of truth for local state.

  4. DesignSystem

    Tokens, themes, type ramp, motion. The vocabulary every feature speaks. New surfaces start by reaching into this package.

  5. LanaiIntelligence

    The only package allowed to import Vision, NaturalLanguage, CoreML, or Apple Intelligence. Every AI capability the app uses sits behind a protocol here. The architectural enforcement of the on-device promise.

  6. PlatformAdapters

    The thin layer where iPhone, iPad, and Mac differences live. Image types, share sheets, haptics. Everything else above the line is universal.

The intelligence seam.

The architectural decision the rest of the app depends on.

Inside

LanaiIntelligence

The only package allowed to import Vision, NaturalLanguage, CoreML, or the Apple Intelligence Foundation Models. Every AI capability lives behind a small protocol here. Implementations are unsurprising. So are the mocks.

Outside

Everyone else

Compose, Timeline, Profile, Settings, Export. Every other package imports the public interface only. The on-device frameworks are out of reach, by build-time guarantee.

The point of the seam isn’t the abstraction. The point is that the privacy posture is enforced at the import graph, not at the description. A future feature that wanted to send something off-device would have to do so visibly, in the one room where it could be reviewed.

What we don’t build.

The architecture is shaped as much by what isn’t there as by what is. A short list of decisions that didn’t happen.