Building blocks for AT Protocol iOS apps. Parse rich text with facets, handle OAuth authentication via a backend, and work with timeline data from Bluesky and other AT Protocol services.
Good fit if you:
- Are building an iOS/macOS app that connects to Bluesky or AT Protocol services
- Need to parse and display rich text posts with links, mentions, and hashtags
- Want BFF (Backend-For-Frontend) OAuth pattern with HttpOnly cookies
- Prefer a lightweight foundation over a full-featured SDK
Less ideal if you:
- Need a complete AT Protocol client with all XRPC methods
- Want to run your own PDS or build server-side tooling
- Need cross-platform support beyond Apple platforms
- Swift 6.0+
- iOS 18.0+ / macOS 14.0+
Add ATProtoFoundation to your Package.swift:
dependencies: [
.package(url: "https://github.com/tijs/ATProtoFoundation.git", from: "1.1.0")
]Or add it via Xcode: File → Add Package Dependencies → Enter the repository URL.
Types for working with Bluesky post records and rich text facets:
import ATProtoFoundation
// Create a record with rich text
let facet = ATProtoFacet(
index: 10...28,
feature: .link("https://example.com")
)
let record = BlueskyPostRecord(
text: "Check out https://example.com for more",
facets: [facet]
)
// Get markdown-formatted text with clickable links
let formatted = record.formattedTextSupported facet features:
.link(url)- External URLs.mention(did)- AT Protocol user mentions.hashtag(tag)- Hashtags with search URLs
Types for parsing timeline API responses:
// Decode timeline data from API
let record = try JSONDecoder().decode(TimelineRecord.self, from: jsonData)
// Convert to rich BlueskyPostRecord with facets
let postRecord = BlueskyPostRecord(from: record)Models for community lexicons - standardized schemas for common data types:
Bookmarks
let bookmark = Bookmark(
subject: "https://example.com/article",
tags: ["reading-list", "tech"]
)Calendar Events
let event = CalendarEvent(
name: "Swift Meetup",
description: "Monthly developer meetup",
startsAt: Date(),
mode: .hybrid,
status: .scheduled,
locations: [.geo(GeoCoordinates(latitude: 37.7749, longitude: -122.4194))]
)
let rsvp = CalendarRSVP(subject: eventRef, status: .going)Interactions
let like = InteractionLike(subject: postRef)Locations
let geo = GeoCoordinates(latitude: 37.7749, longitude: -122.4194, name: "San Francisco")
let address = CommunityAddress(country: "US", locality: "San Francisco", street: "123 Main St")
let fsq = FoursquareLocation(fsqPlaceId: "4b8c3d87f964a520f7c532e3")
let h3 = H3Location(value: "8928308280fffff")Payments
let wallet = WebMonetizationWallet(
address: "https://ilp.uphold.com/abc123",
note: "Support my work"
)Extensible constants for AT Protocol lexicon identifiers:
// Bluesky lexicons
BlueskyLexicon.feedPost // "app.bsky.feed.post"
BlueskyLexicon.richTextLink // "app.bsky.richtext.facet#link"
BlueskyLexicon.richTextMention // "app.bsky.richtext.facet#mention"
BlueskyLexicon.richTextTag // "app.bsky.richtext.facet#tag"
// Community lexicons
CommunityLexicon.bookmark // "community.lexicon.bookmarks.bookmark"
CommunityLexicon.calendarEvent // "community.lexicon.calendar.event"
CommunityLexicon.calendarRSVP // "community.lexicon.calendar.rsvp"
CommunityLexicon.interactionLike // "community.lexicon.interaction.like"
CommunityLexicon.locationGeo // "community.lexicon.location.geo"
CommunityLexicon.locationAddress // "community.lexicon.location.address"
CommunityLexicon.locationFoursquare // "community.lexicon.location.fsq"
CommunityLexicon.locationH3 // "community.lexicon.location.hthree"
CommunityLexicon.webMonetization // "community.lexicon.payments.webMonetization"Detect and create facets from text:
let processor = RichTextProcessor()
let facets = processor.detectFacets(in: "Check out https://example.com and @alice.bsky.social #swift")
// Returns RichTextFacet array with links, mentions, and hashtagsBackend-For-Frontend (BFF) pattern OAuth implementation using HttpOnly cookies. Mobile apps can't safely store private keys, so the recommended approach is to have a backend server manage OAuth sessions and use session cookies to authenticate the mobile client.
For the server-side implementation, see @tijs/atproto-oauth and its mobile authentication guide:
let config = OAuthConfiguration(
baseURL: URL(string: "https://your-backend.app")!,
userAgent: "YourApp/1.0 (iOS)",
sessionCookieName: "sid",
cookieDomain: "your-backend.app",
callbackURLScheme: "your-app",
sessionDuration: 86400 * 7,
refreshThreshold: 3600,
maxRetryAttempts: 3,
maxRetryDelay: 8.0
)
let coordinator = MobileOAuthCoordinator(
storage: KeychainCredentialsStorage(),
config: config
)
// Start OAuth flow
let authURL = try await coordinator.startOAuthFlow()
// Handle callback
try await coordinator.completeOAuthFlow(callbackURL: url)
// Refresh session
let credentials = try await coordinator.refreshSession()Protocol-based storage with multiple implementations:
// Keychain storage (recommended for production)
let storage = KeychainCredentialsStorage()
// In-memory storage (for testing)
let storage = InMemoryCredentialsStorage()
// Custom storage - implement CredentialsStorageProtocolCentralized OAuth configuration:
let config = OAuthConfiguration(
baseURL: URL(string: "https://your-backend.app")!,
userAgent: "YourApp/1.0 (iOS)",
sessionCookieName: "sid",
cookieDomain: "your-backend.app",
callbackURLScheme: "your-app",
sessionDuration: 86400,
refreshThreshold: 3600,
maxRetryAttempts: 3,
maxRetryDelay: 8.0
)HTTP client with automatic session management:
let client = BFFAPIClient(
credentialsStorage: storage,
config: config // Use same OAuthConfiguration as coordinator
)
// GET request
let data = try await client.get(endpoint: "/api/endpoint")
// POST request with JSON
let response = try await client.post(
endpoint: "/api/endpoint",
body: someEncodable
)Flexible ISO8601 date parsing for API responses:
// Handles both formats:
// - With fractional seconds: "2025-01-15T12:30:45.123Z"
// - Without fractional seconds: "2025-01-15T12:30:45Z"
let date = ISO8601DateFormatter.flexibleDate(from: dateString)Protocol-based logging with debug and silent implementations:
// Debug logger (prints to console in debug builds)
let logger = DebugLogger()
// Silent logger (no output)
let logger = SilentLogger()
// Custom logger - implement Logger protocolATProtoFoundation follows these principles:
- Protocol-first: All major components use protocols for testability
- Dependency injection: Services accept dependencies through initializers
- Sendable by default: Types are designed for Swift 6 strict concurrency
- No external dependencies: Uses only Foundation and Security frameworks
Run tests:
swift testThe package includes comprehensive tests for all components using Swift Testing framework.
MIT License - see LICENSE file for details.