Skip to main content
Full technical details in your project at /docs/modules/Storage.md

What You Get

  • Repository pattern - Never expose @Model types to Views
  • DTO pattern - Lightweight, Sendable data transfer objects
  • Keychain wrapper - Secure, tested token storage
  • Cursor pagination - Efficient infinite scroll
  • Batch operations - Optimized bulk updates/deletes
  • Migration framework - Safe schema evolution
  • Optional cloud sync - Supabase integration (feature-flagged)
Time saved: 16-24 hours of implementing and testing repositories, pagination, Keychain, and migrations.

Production Setup (From Real Code)

Here’s how the boilerplate actually sets up storage in CompositionRoot.swift:
// 1. Define schema with all models
let schema = Schema([
    Conversation.self,
    Message.self,
    Settings.self
])

// 2. Create persistent container
let modelConfiguration = ModelConfiguration(
    schema: schema,
    isStoredInMemoryOnly: false  // Persists to disk
)

self.modelContainer = try ModelContainer(
    for: schema,
    configurations: [modelConfiguration]
)

// 3. Create repositories (not exposed directly to Views)
let mainContext = modelContainer.mainContext

self.conversationRepository = ConversationRepositoryImpl(
    modelContext: mainContext
)
self.messageRepository = MessageRepositoryImpl(
    modelContext: mainContext
)
self.settingsRepository = SettingsRepositoryImpl(
    modelContext: mainContext
)

// 4. Keychain for secure token storage
self.keychainStore = KeychainStore(accessGroup: nil)

Key Architecture Decisions

  • @Model types are internal - never exposed to Views
  • DTOs are public - lightweight, Sendable structs
  • Repositories on @MainActor - SwiftData requirement
  • Protocol-based - easy to test with mocks
  • Single ModelContext - thread-safe, no conflicts

Repository Pattern (Production Implementation)

The boilerplate uses protocol-based repositories that return lightweight DTOs:
// Protocol (public API)
public protocol ConversationRepository: Sendable {
    func create(title: String, personaName: String?) async throws -> ConversationDTO
    func rename(id: UUID, title: String) async throws
    func delete(id: UUID) async throws
    func list(limit: Int, after: Date?) async throws -> [ConversationDTO]
}

// DTO (lightweight, Sendable)
public struct ConversationDTO: Identifiable, Sendable, Equatable {
    public let id: UUID
    public let title: String
    public let personaName: String?
    public let createdAt: Date
    public let updatedAt: Date
}

// Implementation (internal)
@MainActor
final class ConversationRepositoryImpl: ConversationRepository {
    private let modelContext: ModelContext
    
    // Uses SwiftData internally, returns DTOs
    // Full error handling and logging
    // Tested comprehensively
}
Why This Pattern:
  • Views never see @Model types (maintains MVVM boundaries)
  • DTOs are Sendable (thread-safe)
  • Easy to mock for testing
  • Can swap implementations (local, cloud, hybrid)

Optional Cloud Sync

  • Enable Chat Sync
  • Enable Photo Sync
Sync conversations and messages across devices:Setup Guide: Chat Sync Setup
  1. Run SQL migration
  2. Enable feature flag
  3. Wire up hybrid repositories
  4. Test cross-device sync

Keychain Storage (Real Implementation)

The boilerplate includes a production-ready Keychain wrapper used for all secure storage:
// From actual code - secure token storage
let keychain = KeychainStore(accessGroup: nil)

// Save (used by Auth module for tokens)
try keychain.setString(accessToken, for: KeychainStore.Keys.authAccessToken)
try keychain.setString(refreshToken, for: KeychainStore.Keys.authRefreshToken)

// Retrieve (used by AuthInterceptor for requests)
let token = try? keychain.getString(KeychainStore.Keys.authAccessToken)

// Delete (on sign out)
try? keychain.delete(KeychainStore.Keys.authAccessToken)

// Standard keys (pre-defined)
KeychainStore.Keys.authAccessToken     // "auth_access_token"
KeychainStore.Keys.authRefreshToken    // "auth_refresh_token"
Production Features:
  • ✅ iOS Security framework (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)
  • ✅ Automatic PII redaction in logs
  • ✅ Thread-safe operations
  • ✅ Comprehensive error handling
  • ✅ Works with AuthInterceptor seamlessly via KeychainTokenProvider

Customization Examples

Add New SwiftData Model

// 1. Create model
@Model
class SavedPrompt {
    @Attribute(.unique) var id: UUID
    var title: String
    var content: String
    var category: String?
    var createdAt: Date
}

// 2. Add DTO
struct SavedPromptDTO: Identifiable, Sendable {
    let id: UUID
    let title: String
    let content: String
    let category: String?
    
    init(_ model: SavedPrompt) {
        self.id = model.id
        self.title = model.title
        self.content = model.content
        self.category = model.category
    }
}

// 3. Create repository
protocol SavedPromptRepository: Sendable {
    func create(title: String, content: String) async throws -> SavedPromptDTO
    func list() async throws -> [SavedPromptDTO]
    func delete(id: UUID) async throws
}

// 4. Add to schema in CompositionRoot
let schema = Schema([
    Conversation.self,
    Message.self,
    Settings.self,
    SavedPrompt.self  // Add this
])

Key Files

ComponentLocation
ModelsPackages/Storage/Sources/Storage/Models/
RepositoriesPackages/Storage/Sources/Storage/Repositories/
KeychainPackages/Storage/Sources/Storage/Keychain/
Cloud SyncPackages/Storage/Sources/Storage/Supabase*

Dependencies

  • Core - Error handling, logging
  • Networking - For cloud sync (optional)

Used By

  • FeatureChat - Conversation and message storage
  • FeatureSettings - Settings persistence
  • Auth - Keychain for tokens
  • Profile - Photo storage (optional)

Best Practices

  • Use DTOs for passing data
  • @MainActor for ModelContext
  • Sendable for repositories
  • Unique IDs with @Attribute(.unique)
  • Protocol-based design
  • Return DTOs (not @Model objects)
  • Async/await throughout
  • Comprehensive error handling
  • Offline-first (local writes fast)
  • Background sync (non-blocking)
  • Graceful degradation
  • Optional (feature flag)

Learn More

Test Coverage

88%+ - Comprehensive repository testing Tests include:
  • CRUD operations
  • Pagination
  • Error scenarios
  • Concurrent access
  • Keychain operations
I