> ## Documentation Index
> Fetch the complete documentation index at: https://docs.swiftaiboilerplate.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Storage Module

> Production-grade data layer with **SwiftData repositories**, **iOS Keychain** for secrets, and **optional cloud sync**.

<Info>
  **Full technical details** in your project at `/docs/modules/Storage.md`
</Info>

## Concurrency notes (v2.2.0)

* **`MessageRepositoryImpl`, `ConversationRepositoryImpl`, and `SettingsRepositoryImpl` are `@MainActor`-pinned.** If downstream code constructs these from a non-main actor, you need an `await` at the construction site. The public protocol API is unchanged.
* Swift 6 strict concurrency across the package.

<Warning>
  If you hit `MainActor-isolated` errors on `ModelContext` after upgrading, it's this change. Build the repository on the main thread or `await` it from a background context.
</Warning>

## 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`:

```swift theme={null}
// 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 stay in the data layer** - Views consume DTOs, never `@Model` objects
* ✅ **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:

```swift theme={null}
// 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 (public, @MainActor-isolated)
@MainActor
public 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

<Tabs>
  <Tab title="Enable Chat Sync">
    Sync conversations and messages across devices:

    **Setup Guide:** [Chat Sync Setup](/pages/guides/chat-sync)

    1. Run SQL migration
    2. Enable feature flag
    3. Wire up hybrid repositories
    4. Test cross-device sync
  </Tab>

  <Tab title="Enable Photo Sync">
    Store profile photos in Supabase Storage:

    **Setup Guide:** [Profile Photos Setup](/pages/guides/profile-photos)

    1. Create storage bucket
    2. Set RLS policies
    3. Enable in CompositionRoot
    4. Test upload/download
  </Tab>
</Tabs>

## Keychain Storage (Real Implementation)

The boilerplate includes a **production-ready Keychain wrapper** used for all secure storage:

```swift theme={null}
// 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 via `KeychainTokenProvider`

## Customization Examples

### Add New SwiftData Model

```swift theme={null}
// 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

| Component           | Location                                                                                                                                         |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Models              | `Packages/Storage/Sources/Storage/Models/`                                                                                                       |
| Repositories        | `Packages/Storage/Sources/Storage/Repositories/`                                                                                                 |
| Keychain            | `Packages/Storage/Sources/Storage/Keychain/`                                                                                                     |
| Cloud sync (chat)   | `Packages/Storage/Sources/Storage/Repositories/Supabase*Repository.swift`, `HybridConversationRepository.swift`, `HybridMessageRepository.swift` |
| Cloud sync (photos) | `Packages/Storage/Sources/Storage/SupabaseProfilePhotoStorageClient.swift`                                                                       |

## Dependencies

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

## Used By

* **FeatureChat** - Conversation and message storage
* **FeatureSettings** - Settings persistence
* **Auth** - Keychain for tokens
* **App target (profile)** - `ProfilePhotoStorageClient` for profile photos (optional)

## Best Practices

<AccordionGroup>
  <Accordion title="SwiftData">
    * Use DTOs for passing data
    * @MainActor for ModelContext
    * Sendable for repositories
    * Unique IDs with @Attribute(.unique)
  </Accordion>

  <Accordion title="Repositories">
    * Protocol-based design
    * Return DTOs (not @Model objects)
    * Async/await throughout
    * Comprehensive error handling
  </Accordion>

  <Accordion title="Cloud Sync">
    * Offline-first (local writes fast)
    * Background sync (non-blocking)
    * Graceful degradation
    * Optional (feature flag)
  </Accordion>
</AccordionGroup>

## Learn More

<CardGroup cols={2}>
  <Card title="Full Documentation" icon="book" href="https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/blob/main/docs/modules/Storage.md">
    Complete Storage guide
  </Card>

  <Card title="Chat Sync" icon="cloud" href="/pages/guides/chat-sync">
    Enable cross-device sync
  </Card>

  <Card title="Photo Storage" icon="image" href="/pages/guides/profile-photos">
    Enable cloud photos
  </Card>

  <Card title="Building Guide" icon="hammer" href="/pages/guides/building-your-app">
    Add custom models
  </Card>
</CardGroup>

## Test Coverage

`StorageTests` runs as part of the workspace suite — **\~598 tests across 12 package test targets + the app test suites** in one `Boilerplate.xctestplan` pass.

Storage tests cover:

* CRUD operations
* Pagination
* Error scenarios
* Concurrent access
* Keychain operations
