An audit for v0.1.0 turned up the same pattern in four places: a computed property doing real work (filtering/sorting/scoring a collection), read multiple times per render including from unrelated state changes (selection, hover, scroll), so the work reran far more often than the underlying data actually changed. Converted each to a @State cache recomputed only via onChange of its real inputs: - DocumentSearchSheet: matchingLineIndices re-scanned the whole document per access, read once per visible row plus twice more in the header/step logic - O(n^2) case-insensitive scan per frame on a large document. Also split into an ordered array (for currentMatchIndex/stepping) plus a parallel Set for the per-row highlight check, which was an O(k) linear .contains before. - CollectionDocumentsOutline: tree rebuilt the whole dictionary- grouped, recursively-sorted document tree on every body evaluation, not just when documents/sortOption actually changed. - CommandPaletteView: results re-scored and re-sorted the entire index (up to the whole local workspace cache in Full Workspace mode) on every render, including ones from selectedIndex moving as arrow keys are pressed. - CollectionOverviewView: sortedDocuments re-sorted on every render; same pattern, smaller blast radius (capped at 100 docs). Also: - HomeViewModel.fetchPinnedThrowing fetched each pinned document serially in a for loop (one round trip at a time) - switched to a TaskGroup so latency doesn't scale with pin count, results reordered back to pins.list's own order since task completion order isn't submission order. - AvatarCropperView.renderFinalImage ran ImageRenderer + JPEG compression synchronously on the main actor from the "Use Photo" button tap. ImageRenderer itself has to stay on the main actor (it captures live SwiftUI state), but JPEG compression on the already- rendered bitmap has no SwiftUI dependency left - hopped that part to a detached Task via tiffRepresentation (plain Data, unlike NSImage itself isn't Sendable) so it doesn't hitch the UI. No crash risks or retain cycles found in the same audit (no try!/ as!, force-unwraps essentially absent outside a hardcoded URL literal, weak self already used where it matters) - this is purely the perf half of the findings. Not compiler-verified - Outpost app target has no CLI build path.
Outpost
A native Apple ecosystem client for Outline — built for iOS, iPadOS, and macOS from a single SwiftUI codebase, aiming for full editing parity with Outline's web app, including realtime collaborative editing.
Early alpha — macOS only for now. Expect missing features and rough edges. iOS/iPadOS support is planned but not in the current build. See the releases page for changelogs, and open an issue if you hit anything.
Why
Outline's web app is great, but there's no native Apple client with full editing parity. This project connects to a self-hosted Outline instance over its REST API and realtime collaboration socket to provide a proper native experience across the Apple ecosystem.
Requirements
- Xcode 27+ (currently developed against an Xcode 27 beta — this is a hard minimum, not a suggestion)
- macOS 27+. iOS/iPadOS support is planned but not in the current build (see the alpha note above) — same 27+ minimum will apply once it lands
- A self-hosted (or hosted) Outline instance with API access
Setup
- Clone the repo and open the
.xcodeprojin Xcode. - Generate a scoped API key on your Outline instance (Settings → API Keys).
- On first launch, enter your instance URL and API key — these are stored in Keychain, never in app config or source.
Architecture
See CLAUDE.md and docs/ARCHITECTURE.md for the full technical breakdown: REST layer, Yjs/Hocuspocus realtime sync via YSwift, and the ProseMirror-schema-to-native-editor mapping.
Disclaimer
This project is being built to solve a personal problem — I wanted a genuinely good native knowledge-base client for my own self-hosted Outline instance and none of the existing options fit. It's a personal-use tool first, not a polished product with support guarantees.
Parts of this codebase are AI-assisted (built with the help of AI coding tools). Treat that however you like — review, use, fork, or ignore the code accordingly. No warranty of quality, security, or fitness for any particular use is implied beyond what's stated in the license below.
License
Outpost itself is licensed under the Business Source License 1.1 — the same license family Outline's own server uses, for the same reason. In short: free to read, self-host, and modify for personal or non-commercial use; not free to repackage as a competing hosted product or to distribute under a name/branding that claims official or affiliated status. It converts automatically to Apache License 2.0 on the change date stated in LICENSE. "Outpost" and its logo are trademarks of the project — see the license's trademark notice.
This project is a client only — it does not include, vendor, or redistribute any of Outline's own (also BSL 1.1 licensed) server source.
Credits
The native markdown editor is built on swift-markdown-engine by Luca Chen, licensed under Apache License 2.0. It's vendored directly in Vendor/swift-markdown-engine (see its LICENSE); local fixes made in that copy haven't been upstreamed yet.
Privacy
Outpost collects nothing about you — no analytics, no telemetry, no crash reporting of its own, no age or demographic data, nothing. The only thing stored locally is your Outline server URL and API token (in the device Keychain) and, optionally, a local offline cache of what you've viewed. Everything else goes straight from your device to whatever Outline server you configure — there's no backend in between, and the developer has no access to your data or your server.
Full policy, terms of service, and data-processing statement are on the wiki:
Not affiliated with Outline
This is an independent, unofficial client. Not affiliated with or endorsed by General Outline, Inc.
