Skip to content

Deep dive

Architecture overview

For the technically curious. Single multiplatform target, local Swift packages, an AT Protocol facade, and on-device intelligence with a constitutional principle.

Most apps that win press for design are quietly built on architectures that wouldn’t survive a code review. The pitch is the artifact; the codebase doesn’t always hold up if you actually open it. This essay exists because Lanai’s claim — the work, in other words, is the resume — implies the codebase must hold up, and the easiest way to make that claim verifiable is to describe how the code is organized and what rules it follows.

Read this if you’d like to know whether Lanai is the kind of app whose structural discipline matches its surface. The short answer is yes. The long answer is below.

Tokens are the source of truth across platforms. A diagram of the package graph will replace this placeholder.

One multiplatform target, not three apps

Lanai is a single Apple-platform target. The same SwiftUI code base produces an iPhone app, an iPad app, a Mac app, and a Vision Pro app. Mac Catalyst is not used; the Mac target is native AppKit-bridged where it needs to be, and SwiftUI where it doesn’t.

The reason this matters is small and large at the same time. Small: one codebase, one design system, one type ramp, one set of accessibility behaviors. Large: the thesis — designed to be sat with, accessibility as design language, felt-quality applied everywhere — is enforceable in one place. We don’t have an iPad implementation that quietly forgot to honor Dynamic Type at AX5. There is no iPad implementation. There is one app, and the iPad is a screen size.

Platform-specific code lives behind PlatformAdapters — a small package that exposes a SwiftUI-shaped API for the operations that genuinely differ across platforms (clipboard, share sheet, haptic feedback, font weight access). Feature code imports PlatformAdapters, never UIKit or AppKit directly. The result is a feature module that’s testable on any platform and that doesn’t carry platform-conditional spaghetti.

Local Swift packages with strict boundaries

The repository contains the app target plus a tree of local Swift packages under Packages/. The packages are not theoretical: they’re the unit of compilation, the unit of testing, and the unit of architectural rule enforcement. Each package has a CLAUDE.md at its root that names the imports allowed, the imports forbidden, and the public API surface. The forbidden list is the load-bearing one.

Examples that are real, not aspirational:

  • DesignSystem can import SwiftUI and the system frameworks it needs for tokens, type, and motion. It cannot import any feature package, any networking package, or anything Bluesky-specific. The design system is the seam between SwiftUI and the rest of the app, and that seam runs in one direction.
  • BlueskyModels holds the plain-Swift representations of posts, profiles, threads, and feeds that the rest of the app uses. It cannot import ATProtoKit. It cannot import any feature package. It is a leaf in the dependency graph.
  • BlueskyKit wraps ATProtoKit (the third-party AT Protocol client) and exposes its own protocols and Swift types. AT Protocol types are never re-exported. A feature package that wants to read a post imports BlueskyKit’s Post, not ATProtoKit’s underlying record. The day the AT Protocol shape changes, the change is contained.
  • Feature packages (Packages/Features/*) can import DesignSystem, BlueskyModels, BlueskyKit, PlatformAdapters, and LanaiIntelligence. They cannot import each other. The export feature does not know the timeline feature exists.

The forbidden-imports list is verifiable, not aspirational. If you grep the codebase for import ATProtoKit, the only matches are inside Packages/BlueskyKit/. If you grep for import Vision or import NaturalLanguage, the only matches are inside Packages/LanaiIntelligence/. If you grep for import UIKit or import AppKit, the only matches are inside Packages/PlatformAdapters/. The package graph is the architecture, and the architecture is enforced by what each module is even allowed to type.

The ATProtoKit facade

Bluesky is an open protocol; ATProtoKit is the open-source Swift client. The protocol is good. The client is good. Neither is a stable enough API to bind feature code to directly. What ships in v1.x might shift. What ships in v2 will shift more. We want the app to be able to absorb that.

So BlueskyKit is the facade. It exposes Post, Profile, Thread, Feed, the rest — Swift types whose shape is ours. It exposes the operations the app needs — fetch the timeline, post a reply, like, repost, bookmark — as protocols (AuthProviding, TimelineProviding, PostInteracting) that feature code injects. Production injects implementations that talk to ATProtoKit underneath. Tests inject mocks.

The advantage is concrete: a feature package writing the reply flow does not care which AT Protocol method underlies it. The feature compiles against PostInteracting. The implementation can be swapped. The protocol can change underneath. The feature does not know.

This is the same pattern Apple uses for the system frameworks themselves — a protocol-shaped public API with multiple concrete implementations, including a mock for tests. We didn’t invent it. We just applied it consistently.

The Intelligence package and its constitution

LanaiIntelligence is the only package allowed to import Vision, NaturalLanguage, CoreML, or the Apple Intelligence Foundation Models. Every AI capability the app uses — smart image cropping, text/photo classification, reading-time estimation, sentence-boundary truncation, alt-text suggestion — sits behind a protocol in this package. The protocols are simple: ImageFocusAnalyzing, ImageContentClassifying, AltTextSuggesting, ReadingTimeEstimating. The implementations are unsurprising: a VisionFocusAnalyzer, a NaturalLanguageReadingTime, and so on. The mocks are also unsurprising. The point is the seam.

The package has a one-sentence constitution that governs every feature that runs through it:

Lanai uses on-device intelligence to deepen the user’s relationship with content they chose, never to choose for them.

Six corollaries follow. Every AI feature must be opt-in, reversible, transparent, on-device, optional (gracefully degrades when Apple Intelligence isn’t available), and in voice (no chatbot framing, no emoji, no “I generated this for you” preambles). Features that fail any of the six don’t ship — not in v1.0, not in v1.x, not ever. The principle is more important than any specific feature.

The corollary that does the most architectural work is on-device. No content sent to Apple’s cloud-AI path. No content trained on by us or by anyone else. This is stricter than Apple’s own apps, by design. It is also enforceable: the package graph forbids feature code from importing the AI frameworks directly, so the on-device guarantee is verifiable by grep.

The other corollary that bears repeating is always indicated. When AI is in the loop, there’s a visible marker. There is no version of Lanai where the user is consuming AI-shaped content unknowingly.

The editorial typography pipeline

EditorialPipeline is the package that applies the typographic-care interventions described in the Typography Philosophy document. The pipeline has five tiers (Tier 0 = render correctly; Tier 5 = full layout and editorial enhancement). Each export template declares its tier; each timeline view declares its tier. The pipeline runs on the post text for display only. The post in the user’s data model is never modified. The post the user sees in the timeline is unchanged in storage, even though the screen renders it with smart quotes and proper dashes.

This is the same architecture: a seam, a protocol, an enforceable rule about where the seam is allowed to live. The architectural pattern is the architecture.

Audit cycles, not after-the-fact reviews

The way the codebase stays in this shape is the audit cycle. Per release, every package’s CLAUDE.md is read against the actual imports, the actual public API, and the actual feature usage. Drift is named. Specific changes are proposed. The audit document is a real artifact — it has a date, a list of findings, and a list of resolutions. The pre-shipping checklist for any release is a list of resolved audit items.

The same audit cycle runs at multiple altitudes. The package-level audit catches import drift and API surface drift. The feature-level audit catches whether a feature is doing what its CLAUDE.md says it does. The design-system audit catches whether the tokens are still being used or whether magic values have crept in. The AI-features audit confirms each AI capability still passes the six corollaries.

This isn’t novel as a process. It is rare as a practice in v1.0 indie apps. The reason it exists here is the reason the rest of the architecture exists: the work either holds up to that level of scrutiny or it doesn’t. We chose to make sure it does.

What you can verify

If you’d like to confirm the claims above without taking them on faith, three checks will do most of the work:

  • Grep for forbidden imports. grep -r "import ATProtoKit" Packages/Features/ should return no matches. grep -r "import Vision" Packages/ should return matches only inside LanaiIntelligence. grep -r "import UIKit" Packages/ should return matches only inside PlatformAdapters.
  • Read any CLAUDE.md. Each package’s CLAUDE.md lists its imports allowed and forbidden, and its public API surface. Compare it to the package’s actual code. The two should match.
  • Run the tests. Each package has a test target. Mocks are first-class. The same protocol that ships in production is the protocol the tests inject against. The mocks are simple objects; the implementations are the same.

The architecture is not the design. But the architecture is the reason the design holds together as the codebase grows. It is also the difference between an app that is clean now and an app that will stay clean. We chose the second one.