ATProtoFoundation

main

Swift package providing foundational types and utilities for AT Protocol (atproto) iOS applications
tijs/ATProtoFoundation

ATProtoFoundation

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.

When to use this package

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

Requirements

  • Swift 6.0+
  • iOS 18.0+ / macOS 14.0+

Installation

Swift Package Manager

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.

Features

Models

BlueskyPostRecord & Facets

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

Supported facet features:

  • .link(url) - External URLs
  • .mention(did) - AT Protocol user mentions
  • .hashtag(tag) - Hashtags with search URLs

Timeline Models

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)

Community Lexicon Models

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"
)

Lexicon Constants

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"

Rich Text Processing

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 hashtags

Authentication

BFF OAuth

Backend-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()

Credentials Storage

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 CredentialsStorageProtocol

Configuration

Centralized 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
)

Networking

BFFAPIClient

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
)

Utilities

Date Parsing

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)

Logging

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 protocol

Architecture

ATProtoFoundation 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

Testing

Run tests:

swift test

The package includes comprehensive tests for all components using Swift Testing framework.

License

MIT License - see LICENSE file for details.

Description

  • Swift Tools 6.0.0
View More Packages from this Author

Dependencies

  • None
Last updated: Thu Apr 09 2026 10:33:26 GMT-0900 (Hawaii-Aleutian Daylight Time)