# Architecture
Source: https://docs.swiftaiboilerplate.com/pages/architecture
System design, patterns, and module structure of SwiftAI Boilerplate Pro
SwiftAI Boilerplate Pro uses modern iOS development patterns with clean separation of concerns.
**Full technical details:** See `docs/foundations/Architecture.md` in your project for complete technical coverage.
## Core Principles
### 1. MVVM Architecture
Clean separation between UI, business logic, and data:
**Benefits:**
* ✅ Views are dumb (no business logic)
* ✅ ViewModels are testable (no UIKit dependencies)
* ✅ Business logic isolated and reusable
* ✅ Clear data flow
### 2. Dependency Injection
Centralized composition with `CompositionRoot`:
```swift theme={null}
/// Central dependency injection container
/// Builds and owns all app-wide singletons and factories
@MainActor
@available(iOS 17.0, *)
public final class CompositionRoot {
// MARK: - Singletons
/// SwiftData model container
public let modelContainer: ModelContainer
/// HTTP client with interceptors
public let httpClient: any HTTPClient
/// Keychain storage
public let keychainStore: KeychainStore
/// Auth client (SessionManager wrapper in production, MockAuthClient in DEBUG)
public let sessionManager: any AuthClient
/// LLM client (ProxyLLMClient when configured, EchoLLMClient otherwise)
public let llmClient: any LLMClient
// ...
}
// View-model factories live in CompositionRoot+Factories.swift:
extension CompositionRoot {
/// Create ChatViewModel with injected dependencies
public func makeChatViewModel(conversationID: UUID) -> ChatViewModel {
ChatViewModel(
conversationID: conversationID,
messageRepository: messageRepository,
llmClient: llmClient,
paymentsStatusProvider: PaymentsStatusAdapter(paymentsClient: paymentsClient)
)
}
}
```
**Benefits:**
* ✅ No global singletons
* ✅ Easy to mock for testing
* ✅ Clear dependency graph
* ✅ Compile-time safety
### 3. Protocol-Oriented Design
All external dependencies use protocols:
```swift theme={null}
/// Main authentication client protocol
@available(iOS 17.0, *)
@preconcurrency
public protocol AuthClient: Sendable {
/// Sign in with Apple ID
func signInWithApple() async throws -> AuthUser
/// Sign in with Google
func signInWithGoogle() async throws -> AuthUser
// ...
}
// Implementations:
// - SessionManager (production actor; wrapped to AuthClient via SessionManagerWrapper)
// - MockAuthClient (DEBUG-only fallback)
```
**Benefits:**
* ✅ Swappable implementations
* ✅ Easy testing with mocks
* ✅ No vendor lock-in
* ✅ Clear contracts
## Module Structure
### 11 Swift Packages
Foundation utilities, errors, logging
HTTP client with interceptors
SwiftData models, repositories
Authentication clients
Subscription management
LLM client protocol
Chat UI, ViewModels; owns `LLMClient`/`LLMMessage`
Sentiment-based rating prompts
Settings and paywall UI
Tokens, components, Liquid Glass, accessibility
Type-safe strings, pluralization, i18n
A 12th package, **TestSupport** (`Packages/TestSupport`), ships shared test
infrastructure (e.g. `URLProtocolStub`). It is test-only — not a reusable product
module — so the "11 Swift Packages cleanly separated for reuse" claim still stands.
**Dependency graph (from `template.manifest.json`):**
```text theme={null}
Core → (no dependencies)
TestSupport → (no dependencies, test-only)
DesignSystem → (no dependencies)
Networking → Core
Storage → Core, Networking
Localization → Core
Payments → Core
FeatureRating → Core, DesignSystem
Auth → Core, Networking, Storage
FeatureChat → Core, Storage, DesignSystem, Localization
AI → Core, Networking, FeatureChat (re-exports FeatureChat)
FeatureSettings → Core, Storage, Auth, Payments, DesignSystem, Localization
```
**Coupling gotcha:** `LLMClient` and `LLMMessage` live in **FeatureChat**, and the
**AI** package re-exports FeatureChat. Removing chat without first relocating those two
types breaks `Packages/AI`. The removal order and module map are documented in
`docs/checklists/APP_STORE_4_3_HARDENING.md`.
**Key principles:**
* ✅ No circular dependencies
* ✅ Core has no dependencies
* ✅ Features depend on services
* ✅ Services depend on infrastructure
## Data Flow
### Chat Message Flow
```text theme={null}
1. User types message → ChatView
2. ChatView calls → ChatViewModel.send()
3. ChatViewModel:
- Appends message → MessageRepository
- Streams AI response → LLMClient.streamResponse(messages:)
- Mutates @Observable state on the main actor
4. View automatically re-renders
```
### Authentication Flow
```text theme={null}
1. User enters credentials → SignInView
2. View calls → AuthViewModel.signIn()
3. AuthViewModel calls → AuthClient.signInWithEmail(email:password:)
4. SessionManager (the AuthClient):
- Calls Supabase API
- Persists tokens → Keychain (via SessionStore)
- Returns AuthUser, emits via authStates()
5. LaunchRouter observes the auth state change
6. Navigation switches to authenticated content
```
## iOS 26 Liquid Glass Layer
The DesignSystem package ships a dedicated Liquid Glass primitive.
**Primitive:** `Packages/DesignSystem/Sources/DesignSystem/Materials/SAIGlass.swift`
* **`SAIGlassStyle`**: `.regular` (default surface) and `.clear` (edge-to-edge hero surfaces).
* **`.saiGlass(...)` modifier**: one-call glass treatment for any view.
* **`SAIGlassContainer`**: merges nearby glass surfaces so adjacent cards, toolbars, and sheets sample the same background instead of stacking visually.
**Availability strategy:**
* **iOS 26+**: uses the native `Glass` material, `glassEffect`, and `GlassEffectContainer`.
* **iOS 17–25**: falls back to SwiftUI `Material` automatically. Same call sites, progressive enhancement at runtime.
The same file also exports `saiScrollEdgeGlass(_:)`, `saiSidebarAdaptable()`, `saiTabBarMinimize(_:)`, and `SAITabBarMinimizeStyle` for shell-level polish.
See [Design System Module](/pages/modules/design-system#liquid-glass) for the full API.
## Swift 6 Concurrency Model
The entire codebase builds under **strict concurrency checking** on the Swift 6 toolchain.
* **`@MainActor`-pinned storage repositories**: `MessageRepositoryImpl`, `ConversationRepositoryImpl`, and `SettingsRepositoryImpl` are all main-actor-isolated.
* **`@Observable` throughout**: view models and observable stores use the `@Observable` macro. No `ObservableObject`, no `@Published`, no `.onReceive`.
* **No `DispatchQueue.main` in app code**: all main-thread work goes through `@MainActor` or `MainActor.run`.
* **Explicit `any`**: every protocol-typed property and parameter uses the `any` keyword, as Swift 6 requires.
* **Async/await throughout**: no completion handlers, structured concurrency for cancellation, `Sendable` where crossing actor boundaries.
```swift theme={null}
@MainActor
@Observable
public final class ChatViewModel {
private let messageRepository: any MessageRepository
private let llmClient: any LLMClient
...
}
```
## Architecture Rule: ≤ 400 Lines Per File
A workspace-wide ceiling keeps every production and test Swift file ≤ 400 lines (enforced by `.swiftlint.yml` `file_length: warning: 400`). When a type grows past that, it moves into extension-split siblings in the same folder.
Examples in the codebase:
* `SessionManager.swift` → `SessionManager+SignIn`, `+Refresh`, `+Persistence`
* `L10n` strings → `L10n+.swift` per nested enum (Auth, Chat, Settings, …)
* `SettingsView.swift` → a 125-line composition root with sections split out
* `ChatViewModel.swift` → `ChatViewModel+Memory.swift`
Why: keeps files scannable, keeps diffs small, keeps agent context windows productive.
## Concurrency Primitives
### Async/Await Throughout
```swift theme={null}
/// Send the current input text.
public func send() async {
// 1. Validate input (skip empty/whitespace)
// 2. Append the user message via MessageRepository
// 3. Stream the AI reply: llmClient.streamResponse(messages:)
// 4. Mutate @Observable state on the main actor as tokens arrive
}
```
**Benefits:**
* ✅ No completion handlers
* ✅ Structured concurrency (cancellable stream tasks)
* ✅ MainActor for UI updates
* ✅ Sendable for thread safety
## Testing Strategy
The whole workspace runs in **one pass** via `Boilerplate.xctestplan`: **\~598 tests across 12 package test targets + the app test suites** (`SwiftAIBoilerplateProTests` unit + `SwiftAIBoilerplateProUITests`). A single `xcodebuild test` covers everything:
```bash theme={null}
xcodebuild test \
-project SwiftAIBoilerplatePro.xcodeproj \
-scheme SwiftAIBoilerplatePro \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.2' \
CODE_SIGNING_ALLOWED=NO
```
The 12 package test targets are: `CoreTests`, `TestSupportTests`, `NetworkingTests`, `StorageTests`, `AuthTests`, `DesignSystemTests`, `FeatureChatTests`, `FeatureRatingTests`, `FeatureSettingsTests`, `PaymentsTests`, `LocalizationTests`, `AITests`. (`TestSupportTests` exercises the shared test-infrastructure package.)
### Unit Tests
Test ViewModels and repositories in isolation, injecting fakes for external clients:
```swift theme={null}
func testSendStreamsReply() async throws {
let viewModel = ChatViewModel(
conversationID: UUID(),
messageRepository: messageRepository,
llmClient: EchoLLMClient() // dev fallback echoes the prompt
)
viewModel.inputText = "Hello"
await viewModel.send()
XCTAssertFalse(viewModel.messages.isEmpty)
}
```
### Integration Tests
Test component interaction through the composition root:
```swift theme={null}
@MainActor
func testCompositionRoot() throws {
let authConfig = AuthConfig(
supabaseURL: URL(string: "https://test.supabase.co")!,
supabaseAnonKey: "test_anon_key"
)
let paymentsConfig = PaymentsConfig(
apiKey: "test_api_key",
entitlementID: "test_entitlement"
)
let composition = try CompositionRoot(
authConfig: authConfig,
paymentsConfig: paymentsConfig
)
// Dependencies are wired
XCTAssertNotNil(composition.httpClient)
XCTAssertNotNil(composition.sessionManager)
// Factories produce view models
let chatVM = composition.makeChatViewModel(conversationID: UUID())
XCTAssertNotNil(chatVM)
}
```
### UI Tests
Test end-to-end flows. UI suites need the `UI_TESTING=1` launch hook so the first-launch onboarding/permission alert does not block them:
```swift theme={null}
func testChatFlow() throws {
let app = XCUIApplication()
app.launchEnvironment["UI_TESTING"] = "1"
app.launch()
app.buttons["Start Chat"].tap()
let textField = app.textFields["messageInput"]
textField.tap()
textField.typeText("Hello AI")
app.buttons["send"].tap()
XCTAssertTrue(app.staticTexts["Hello AI"].waitForExistence(timeout: 5))
}
```
## Customization Entry Points
**App Generator (coming soon, separate product).** A standalone macOS app that drives
your own coding-agent CLI (Claude Code / Codex / Gemini / Cursor) to generate a
differentiated, App Store Guideline 4.3(a)-safe app *from* this template — using the
module graph and recipes documented above. It is a separate purchase, not bundled in the
boilerplate, and ships no LLM. See [License & Updates](/pages/reference/license) for the
pricing model (grandfathered buyers get access when it ships).
### Add New Feature
1. **Create package** (if needed): `Packages/FeatureX/`
2. **Create models**: Define SwiftData models in `Storage`
3. **Create repository**: Data access layer
4. **Create ViewModel**: Business logic
5. **Create View**: SwiftUI interface
6. **Wire in CompositionRoot**: Add factory method
### Replace Backend
Implement the protocol:
```swift theme={null}
// Want to use Firebase instead of Supabase? Conform to the AuthClient protocol.
final class FirebaseAuthClient: AuthClient {
func signInWithEmail(email: String, password: String) async throws -> AuthUser {
// Firebase implementation
}
// ... signInWithApple, signInWithGoogle, signOut, currentUser, authStates, etc.
}
// Then wire it in CompositionRoot:
self.sessionManager = FirebaseAuthClient(...)
```
This swap snippet is illustrative — there is no `FirebaseAuthClient` in the template.
The production implementation is `SessionManager` (a Supabase-backed actor) adapted to
`AuthClient` via `SessionManagerWrapper`.
### Add UI Components
1. Create component in `DesignSystem/Components/`
2. Use design tokens (DSColors, DSSpacing, etc.)
3. Add preview
4. Add snapshot test
5. Use in features
## Best Practices Applied
* Files ≤ 400 lines (extension-split when larger)
* One type per file
* MARK comments for navigation
* Grouped by responsibility
* All errors mapped to `AppError`
* User-friendly messages
* Technical details logged
* Never swallow errors silently
* Clear, explicit names
* No abbreviations in public API
* Consistent conventions
* Self-documenting code
* Lazy loading (messages, images)
* Cursor-based pagination
* Image compression
* Offline-first data access
* Background sync
* Tokens in Keychain (never UserDefaults)
* API keys server-side only
* PII redacted in logs
* Row Level Security (Supabase)
## Key Files
| Purpose | File Location |
| ------------------------ | --------------------------------------------------------- |
| **Dependency Injection** | `SwiftAIBoilerplatePro/Composition/CompositionRoot.swift` |
| **Feature Flags** | `SwiftAIBoilerplatePro/Composition/FeatureFlags.swift` |
| **App Entry** | `SwiftAIBoilerplatePro/SwiftAIBoilerplatePro.swift` |
| **Navigation** | `SwiftAIBoilerplatePro/AppShell/MainTabView.swift` |
| **Auth Router** | `SwiftAIBoilerplatePro/AppShell/LaunchRouter.swift` |
## Learn More
Error handling, logging, utilities
HTTP client architecture
SwiftData and repositories
Guidelines for AI-assisted development
**Find full technical documentation in your project:** [Architecture.md](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/blob/main/docs/foundations/Architecture.md)
# AI-Assisted Development
Source: https://docs.swiftaiboilerplate.com/pages/guides/ai-assisted-development
Build faster with Claude Code, Xcode Coding Intelligence, Cursor AI, and Bolt AI. Pre-configured CLAUDE.md, .cursor rules, and .bolt chat modes included.
SwiftAI Boilerplate Pro ships with configurations for Claude-powered iOS development across **Claude Code**, **Xcode Coding Intelligence**, **Cursor AI**, and **Bolt AI**. Each setup points the assistant at the same project architecture so generated code fits the boilerplate instead of guessing.
No setup required. Open the project and start building.
Building a separate app from this template? The **App Generator** is a standalone macOS product (coming soon) that drives your own coding-agent CLI to scaffold a differentiated, App Store Guideline 4.3-safe app from the boilerplate. See [Pricing & License](/pages/reference/license) for details and the waitlist.
***
## Recommended Setup
For most SwiftAI Boilerplate Pro projects, start here:
Open the folder that contains `AGENTS.md`, `SwiftAIBoilerplatePro.xcodeproj`, `Packages/`, and `docs/`. The root `AGENTS.md` is the shared entry point that points every agent at `docs/CLAUDE.md` and `docs/INDEX.md`.
Xcode Coding Intelligence with Claude Agent is a strong fit for Swift apps because it can stay close to Xcode's project, build, simulator, and diagnostics workflow.
Claude Code is best when you want the agent to inspect many files, implement a full feature, run commands, and iterate until the project builds.
Cursor is useful for inline edits, local refactors, and reviewing generated code while you are already working in the editor.
If Claude says it does not see the project instructions, ask it to summarize `docs/CLAUDE.md`. If it cannot, restart the agent from the repository root rather than from a subdirectory or a single file.
## Tool Comparison
| Tool | Best For | Project Context |
| ----------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| **Xcode Coding Intelligence** | Swift app work inside Xcode, build issues, simulator-aware iteration | Reads root `AGENTS.md` → `docs/CLAUDE.md` when started from the project root |
| **Claude Code** | Larger autonomous coding tasks and verification loops | Reads root `AGENTS.md`, which points to `docs/CLAUDE.md` |
| **Cursor AI** | Inline edits, refactors, and code review while editing | Reads `.cursor/rules/*.mdc` (plus `AGENTS.md`) |
| **Bolt AI chat modes** | Specialized chat personas and prompt-driven workflows | Uses `.bolt/chat-modes.json` and project files you provide as context |
***
## Claude Code and Claude Agent (`CLAUDE.md`)
The boilerplate includes a production-tested `docs/CLAUDE.md` that gives [Claude Code](https://claude.ai/code) full project context. A root `AGENTS.md` is the shared entry point all agents read first; it points Claude Code, Cursor, Codex, and others at `docs/CLAUDE.md` and `docs/INDEX.md`, so generated code fits the boilerplate from the first message.
### What CLAUDE.md Covers
* **Architecture:** MVVM patterns, module boundaries, which abstractions to use
* **Build commands:** Environment-aware instructions for Xcode MCP vs Claude Code CLI
* **Anti-patterns:** No UIKit, no ObservableObject, no raw URLSession
* **File conventions:** Naming patterns, organization, where new code belongs
* **Integration specifics:** Supabase, RevenueCat, OpenRouter configuration
### How Agentic Development Works
1. Open Claude Code in your project directory
2. Describe the feature you want
3. Claude reads your codebase, generates code following your patterns, builds it, and iterates until it works
Claude understands the module boundaries, uses the right abstractions, and writes code that looks like the rest of your project.
### Example Workflow
```text theme={null}
You: "Add a favorites feature. Users should be able to bookmark conversations
and see them in a dedicated tab."
Claude Code:
- Reads AGENTS.md / docs/CLAUDE.md and the existing module structure
- Creates SwiftData model in Storage package
- Adds FavoritesRepository with protocol
- Builds FavoritesViewModel (@Observable) following MVVM pattern
- Creates FavoritesView using DesignSystem tokens
- Wires dependencies through CompositionRoot
- Runs build to verify compilation
```
### Community Skills
You can extend Claude's iOS knowledge with community skills. These teach Claude about platform conventions, Xcode tooling, and SwiftUI best practices.
Apple platform conventions, Xcode project structure, SwiftUI, and App Store review awareness.
```bash theme={null}
git clone https://github.com/rshankras/claude-code-apple-skills.git
mkdir -p .claude/skills
cp -R claude-code-apple-skills/skills/* .claude/skills/
```
Source: [rshankras/claude-code-apple-skills](https://github.com/rshankras/claude-code-apple-skills)
Lets Claude boot, build, install, and interact with the iOS Simulator directly.
```text theme={null}
/plugin marketplace add conorluddy/ios-simulator-skill
/plugin install ios-simulator-skill@conorluddy
```
Source: [conorluddy/ios-simulator-skill](https://github.com/conorluddy/ios-simulator-skill)
Run `/plugin ...` commands inside Claude Code or Claude Agent chat. Run `git`, `cp`, and `npx` commands in your terminal. For the full list of recommended skills and module mappings, see the [Agentic Development Setup guide](/pages/guides/skills-setup).
***
## Cursor AI Rules (`.cursor/rules/`)
[Cursor AI](https://cursor.sh) reads `.mdc` (Markdown Code) files from your project to understand patterns and constraints. Six rule files are included.
### Included Rule Files
**Applied to:** All Swift files
File size limits (max 400 lines), MVVM patterns, dependency injection, error handling, Swift Concurrency, testing requirements.
**Applied to:** View files (`*View.swift`)
View composition, state management, list optimization, navigation, form handling, accessibility.
**Applied to:** Repositories, Clients, Packages
Modular package boundaries, protocol-based APIs, repository pattern, CompositionRoot, structured logging, test doubles.
**Applied to:** DesignSystem, AppShell
DSColors tokens, DSTypography type scale, DSSpacing tokens, component library usage, theme consistency.
**Applied to:** Swift, config, and Markdown files
Guideline 4.3(a) differentiation (binary, metadata, concept), the `APP_STORE_4_3_HARDENING.md` checklist, and the FeatureChat to AI coupling when removing chat.
**Applied to:** Swift, `Info.plist`, `PrivacyInfo.xcprivacy`
Public APIs only, Swift 6 concurrency, permission strings and privacy manifests, StoreKit/IAP for digital goods, accessibility baselines, and secrets via the existing `HTTPClient` + xcconfig.
### How It Works
Open the project in Cursor. The rules load automatically. Ask Cursor to build something, and it follows your architecture.
```text theme={null}
You: "Create a new chat settings view"
Cursor generates code with:
- DSColors, DSTypography, DSSpacing (DesignSystem tokens) ✅
- Protocol-based dependencies (testable) ✅
- Under 400 lines (properly composed) ✅
- Loading and error states ✅
- Accessibility labels ✅
```
### Example Prompts
```text theme={null}
"Create a new settings view using DesignSystem components"
"Add a profile page with avatar, name, email, and sign-out"
"Build a subscription paywall with three tiers"
```
Cursor will use DSColors for colors, DSTypography for type, DSSpacing for layout, and create a proper `@Observable` ViewModel with loading and error states.
```text theme={null}
"Add a ProfileViewModel with user info and sign-out"
"Create SettingsViewModel with theme switching"
"Build ChatViewModel with message streaming"
```
Cursor will mark ViewModels as `@MainActor @Observable`, inject dependencies via initializer, use protocol types, and add structured logging.
```text theme={null}
"Create a new feature package for Notifications"
"Add a feature package for User Profile"
```
Cursor will create proper Package.swift, define protocol-based public APIs, keep implementation internal, and add tests.
```text theme={null}
"Refactor this to use repository pattern"
"Extract this logic to a separate package"
"Convert this to use DesignSystem components"
```
***
## Bolt AI Chat Modes (`.bolt/`)
[Bolt AI](https://bolt.new) provides **12 specialized AI assistants** for different development tasks. Each mode loads relevant context and uses a domain-specific system prompt.
### Available Chat Modes
`/dev` (default). General iOS/SwiftUI development with MVVM and async/await.
`/arch`. Package boundaries, protocol-based APIs, clean architecture.
`/ui`. View composition, DesignSystem usage, state management, accessibility.
`/feature`. Self-contained feature modules with protocol APIs and tests.
`/integration`. Supabase, RevenueCat, Firebase setup and troubleshooting.
`/test`. Unit tests, mock protocols, test doubles, async testing.
`/debug`. Bug diagnosis, crash analysis, memory leaks, performance.
`/perf`. Memory management, lazy loading, rendering optimization.
`/package`. Swift Package Manager, API design, dependencies.
`/deploy`. App Store submission, TestFlight, provisioning, CI/CD.
`/a11y`. VoiceOver, Dynamic Type, inclusive design, WCAG compliance.
`/doc`. Technical writing, API docs, integration guides.
***
## Before and After
Without configurations, AI generates code with hardcoded colors, no ViewModel, direct singleton dependencies, and no error handling. With configurations, the same prompt produces code that follows your architecture.
### Without Configurations
**Prompt:** "Create a user profile view with avatar and sign-out"
```swift theme={null}
struct ProfileView: View {
@State var user: User?
var body: some View {
VStack {
Image(systemName: "person.circle")
.font(.system(size: 80))
.foregroundColor(.blue) // ❌ Hardcoded color
Text(user?.name ?? "")
.font(.title) // ❌ Not from DesignSystem
Button("Sign Out") { // ❌ No error handling
AuthManager.shared.signOut() // ❌ Direct dependency
}
.padding()
.background(Color.red) // ❌ Hardcoded color
}
}
}
```
### With Configurations
**Same prompt**, same AI tool:
```swift theme={null}
import SwiftUI
import DesignSystem
import Auth
import Core
struct ProfileView: View {
@State private var viewModel: ProfileViewModel
var body: some View {
ScrollView {
VStack(spacing: DSSpacing.lg) {
Circle()
.fill(DSColors.primary)
.frame(width: 80, height: 80)
.overlay {
Text(viewModel.user?.name?.prefix(1).uppercased() ?? "")
.font(DSTypography.titleL)
.foregroundStyle(.white)
}
VStack(spacing: DSSpacing.sm) {
Text(viewModel.user?.name ?? "Loading...")
.font(DSTypography.titleM)
if let email = viewModel.user?.email {
Text(email)
.font(DSTypography.body)
.foregroundStyle(DSColors.textSecondary)
}
}
SAIButton("Sign Out", style: .secondary, layout: .block) {
Task { await viewModel.signOut() }
}
.padding(.top, DSSpacing.lg)
}
.padding(DSSpacing.lg)
}
.navigationTitle("Profile")
.alert("Error", isPresented: Binding(
get: { viewModel.errorMessage != nil },
set: { if !$0 { viewModel.errorMessage = nil } }
)) {
Button("OK") { viewModel.errorMessage = nil }
} message: {
Text(viewModel.errorMessage ?? "")
}
.task { await viewModel.load() }
}
}
@MainActor
@Observable
final class ProfileViewModel {
var user: AuthUser?
var isLoading = false
var errorMessage: String?
private let authClient: any AuthClient
init(authClient: any AuthClient) {
self.authClient = authClient
}
func load() async {
isLoading = true
defer { isLoading = false }
user = await authClient.currentUser()
}
func signOut() async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
try await authClient.signOut()
AppLogger.info("User signed out", category: AppLogger.auth)
} catch {
errorMessage = AppError.from(error).localizedUserMessage
AppLogger.error("Sign out failed: \(error)", category: AppLogger.auth)
}
}
}
```
DesignSystem tokens, protocol-based dependencies, proper MVVM with `@Observable`, localized error handling, loading state, structured logging, `@MainActor`, async/await. All from the same one-line prompt.
***
## Customization
### Modifying Cursor Rules
Edit `.mdc` files in `.cursor/rules/`. Each file has YAML frontmatter (`description`, `globs`, `alwaysApply`) followed by markdown sections with good/bad code examples. Changes take effect when you save.
### Modifying CLAUDE.md
Edit `docs/CLAUDE.md` (or the root `AGENTS.md` entry point). Add your own patterns, anti-patterns, or module-specific instructions. Claude picks up changes on the next conversation.
### Adding Bolt Chat Modes
Edit `.bolt/chat-modes.json` to add custom modes:
```json theme={null}
{
"chatModes": {
"my-mode": {
"name": "My Mode",
"description": "What this mode does",
"systemPrompt": "Instructions for AI",
"context": ["paths/to/focus/on"],
"tools": ["codeGeneration", "fileManagement"]
}
}
}
```
***
## Documentation
CLAUDE.md details and community skills
Detailed guide on Cursor AI rules
The patterns AI follows
Design tokens reference
***
## Questions
None of them are required. All are supported. **Xcode Coding Intelligence** is a great default for Swift work inside Xcode. **Claude Code** works best for agentic development, such as building full features from a description. **Cursor** works best for inline code generation while you edit. **Bolt** works best for chat-based task switching. Use whichever fits your workflow, or combine them.
Yes. The root `AGENTS.md` is read by most coding agents, and the patterns in `docs/CLAUDE.md` and the `.mdc` files work as reference for any AI tool. You can paste relevant sections into ChatGPT, Copilot, or any assistant that accepts context.
The opposite. AI configurations make code generation faster and more accurate because the AI gets it right the first time. Less time fixing, more time shipping.
Yes. Edit `docs/CLAUDE.md` (or root `AGENTS.md`), `.cursor/rules/*.mdc`, or `.bolt/chat-modes.json` to match your preferences. Changes take effect immediately (Cursor, Bolt) or on the next conversation (Claude Code).
No. These configurations only guide AI code generation. They don't lint or enforce anything on code you write by hand.
***
## Next Steps
Use AI to build a feature with your project's patterns
Understand the patterns AI follows
Let AI write tests for your features
# Before you submit: App Store Guideline 4.3 hardening
Source: https://docs.swiftaiboilerplate.com/pages/guides/app-store-4-3-hardening
How to ship an app built on SwiftAI Boilerplate Pro without getting flagged for spam or template similarity under App Store Review Guideline 4.3. Binary string audits, branding maps, metadata, and an LLM cleanup prompt.
Apple reviews change. This page reduces the patterns reviewers cite most under [Guideline 4.3](https://developer.apple.com/app-store/review/guidelines/#4.3); it does not guarantee approval. Treat it as a checklist, not a legal opinion.
## What 4.3 actually says
Apple's [Guideline 4.3 (Spam)](https://developer.apple.com/app-store/review/guidelines/#4.3) has two parts. **4.3(a)** says don't ship multiple Bundle IDs of the same app. **4.3(b)** says don't pile into a saturated category without a unique, high-quality experience. There's a related rule, **4.2.6**, about apps "created from a commercialized template or app generation service."
Reviewers don't always cite the sub-letter. When they say "4.3," they usually mean one of three things they noticed:
1. Your binary, listing, and screenshots look too similar to another app already on the store.
2. Your product is functionally identical to a known template (same screens, same flows, same strings).
3. The category is saturated and your app doesn't show a clear reason to exist.
## Shared architecture is fine. Undifferentiated product is not.
Thousands of apps share SwiftUI, RevenueCat, Supabase, and OpenRouter. Apple knows this. What gets flagged is when the **product, listing, and binary fingerprint** all look like a copy of something else. Shared architecture alone does **not** guarantee approval — differentiation is **product + listing + binary**.
Your job before submitting:
* Keep the architecture (it's yours now anyway).
* Replace every user-visible string, asset, and flow that screams "boilerplate."
* Lead your App Store listing with **your** hero feature, not a generic chat screen.
* Strip dead code that ships in the binary and exposes the boilerplate's name.
## What v2.2.0 ships to help you
v2.2.0 makes 4.3(a) hardening a first-class, automatable workflow instead of a manual grep:
Builds a **Release** binary, scans the main executable, every app-owned bundle file, and bundled legal/demo markdown for starter fingerprints, then exits non-zero on blocking app-owned hits (and until you replace the starter `BrandConfig.appDisplayName`, set production legal URLs, and rename the upstream bundle ID).
Machine-readable single source of truth at the repo root: identity surface, module graph (removable + couplings), config schema, the canonical fingerprint list, differentiation rules, recipes, and verification commands. The audit script reads its fingerprint list from here so the script, CI, and agent prompts never diverge.
The full playbook: binary audit, branding/identity map, metadata, App Review Notes template, and per-module removal notes (including coupling rules).
Prompt packs for your coding agent, including the must-run starter-fingerprint cleanup (`SwiftAI`, `Boilerplate`, `DesignSystem/SAI`).
The **App Generator** ([a separate, standalone macOS product, coming soon](/pages/reference/license)) automates this entire pass — it drives your own coding-agent CLI to generate a differentiated, 4.3-safe app from the template. It ships no LLM and is not bundled in the boilerplate. The steps below are the manual workflow you can run today on any copy of the source.
## The steps to take before you hit "Submit"
Before anything manual, run the bundled audit. It builds a Release configuration, scans the binary and app bundle, and fails on blocking app-owned fingerprints.
```bash theme={null}
# From your distribution repo root
bash scripts/appstore-43-audit.sh
```
The upstream starter is **expected to fail** until you configure app-specific branding, legal URLs, backend URLs, and bundle identity. The script reports four buckets: main-executable risky strings, app-owned bundle risky strings, bundled legal/demo markdown, and configuration readiness. Third-party SDK noise is reported separately and is non-blocking. Re-run it until every app-owned hit is gone or intentionally documented.
The audit script does this for you, but if you want to inspect the binary by hand, reviewers' automated checks compare binary fingerprints. Anything that ships in your Release `.ipa` can match boilerplate fingerprints.
```bash theme={null}
# Build a Release archive in Xcode, then export the .ipa
unzip -o YourApp.ipa -d /tmp/yourapp
strings -a /tmp/yourapp/Payload/YourApp.app/YourApp \
| grep -i -E "swiftai|boilerplate|designsystem/sai|echollm|mockauth" \
| sort -u
```
The canonical fingerprint list lives in `template.manifest.json` under `fingerprints.patterns`. If the output contains the boilerplate's product name, the `SAI` design-system prefix, dev-only client names, or stub strings, fix the source and rebuild. Don't ship an `.ipa` whose strings give away its origin.
Files that aren't referenced by your app's UI but are still in the **app target** still compile into the binary. That includes example views, sample data, and demo flows you replaced.
Open Xcode → Project navigator → check Target Membership for every file in `Features/`, `Examples/`, and any `*Sample*.swift`. Anything you don't use, remove from the app target (you can keep the file on disk if you want it for reference).
See the per-module removal notes in `docs/checklists/APP_STORE_4_3_HARDENING.md` in your distribution repo. Removing a module without pruning its couplings leaves orphaned code paths in the binary — see the module-coupling step below before deleting FeatureChat, AI, or Payments.
Search the codebase for the boilerplate's product name and replace it everywhere a user can see it: launch screen, onboarding, settings, paywall, push copy, error toasts, support email, About screen, legal pages.
```bash theme={null}
# In your project root
rg -i "swiftai|boilerplate" \
--type swift --type plist \
--glob '*.strings' \
--glob '!**/Tests/**' --glob '!**/.build/**'
```
Build your **branding map** before you start: old name → new name, old bundle ID → new bundle ID, old support URL → new support URL. Apply it once. Don't leave half-renamed files.
The boilerplate's design-system types use an `SAI` prefix (for example `SAIGlass`), which surfaces in the binary as `DesignSystem/SAI`. Rebrand the prefix with the bundled script — dry-run first, then apply after committing your work:
```bash theme={null}
bash scripts/rebrand-design-system-prefix.sh SAI YourAppPrefix --dry-run
bash scripts/rebrand-design-system-prefix.sh SAI YourAppPrefix --apply
```
The boilerplate ships a chat-first home. If your app is a parking helper, a meal planner, or a study tool, **chat is not your hero**. Reviewers see five other AI-chat apps a day; if your first screenshot is "AI chat with bubbles," you're invisible.
Differentiation here is enforced by **mandatory archetype selection, not randomness**. `template.manifest.json` flags `differentiation.archetypeRequired`, and the repo ships hero recipes in `docs/recipes/hero/` to rebuild the home screen around a distinct primary experience:
* `DashboardGrid.md` — metrics/cards home
* `FeedTimeline.md` — scrolling feed
* `MapCanvas.md` — map-centric UI
* `GuidedTaskFlow.md` — step-by-step flow
* `ConversationalHome.md` — chat-first (only if chat genuinely is your product)
First screenshot: the screen that shows what your app actually does (parking spot map, meal of the day, flashcard review). Second screenshot: the differentiated outcome. Chat, if it appears at all, comes after.
Removing a module without its couplings leaves dangling symbols in the binary, which reviewers' tooling can fingerprint. The biggest gotcha:
`LLMClient` and `LLMMessage` live in **FeatureChat**, and the **AI** package re-exports FeatureChat (`@_exported import FeatureChat` in `Packages/AI/Sources/AI/AI.swift`). You **cannot delete FeatureChat alone** without first moving `LLMClient` / `LLMMessage` into `AI` (or `Core`) and dropping the `FeatureChat` dependency from `Packages/AI/Package.swift`. Otherwise `Packages/AI` stops compiling.
If you remove **Payments**, you also need to remove paywall presentation hooks from **FeatureSettings** (which depends on Payments). The full per-module table, including which symbols to grep for and what to delete, is in `docs/checklists/APP_STORE_4_3_HARDENING.md`. Pair it with the buyer-facing prompts in `docs/prompts/AppStore4_3Hardening.prompts.md`.
## Pre-submit checklist
* [ ] `bash scripts/appstore-43-audit.sh` reports **PASS** (no blocking app-owned hits)
* [ ] `strings` on the Release `.ipa` returns no `swiftai`, `boilerplate`, `designsystem/sai`, or stub matches
* [ ] Bundle ID is yours, not the boilerplate's `com.berkin.SwiftAIBoilerplatePro`
* [ ] No demo or sample files are in the app target's Compile Sources
* [ ] Removed modules have their couplings pruned (see module table)
* [ ] Branding map applied: name, support email, URLs, paywall copy
* [ ] Onboarding pages reflect your product, not generic AI-chat copy
* [ ] Settings → About shows your company, your URLs, your version
* [ ] Push notifications and email templates use your wording
* [ ] First screenshot leads with your hero, not the chat screen
* [ ] App name and subtitle describe your product, not "AI chat"
* [ ] Description's first line answers "what does this app do for me"
* [ ] Privacy policy and terms hosted at your domain (not boilerplate's)
* [ ] You explain in plain language what the app does and who it's for
* [ ] You credit the architecture honestly if asked, while making clear the product, content, and listing are yours
* [ ] Demo account credentials work and have realistic data
## Example LLM cleanup prompt
### Must-run starter fingerprint prompt
If the Release audit reports `SwiftAI`, `Boilerplate`, or `DesignSystem/SAI`, run this first. These are starter fingerprints and should not remain in app-owned Release output.
```markdown theme={null}
I am preparing an iOS app derived from SwiftAI Boilerplate Pro for App Store submission. The Release audit still reports these blocking app-owned fingerprints:
- SwiftAI
- Boilerplate
- DesignSystem/SAI
My app details:
- App name: [FILL IN]
- Bundle identifier: [FILL IN]
- Design-system prefix to use instead of SAI: [FILL IN, e.g. MYA]
- Primary app purpose: [FILL IN]
Tasks:
1. Search the production app target, linked local packages, app extension targets, generated configuration, launch storyboard, asset names, Info.plist values, xcodeproj settings, and package resources for exact matches of `SwiftAI`, `Boilerplate`, and `DesignSystem/SAI`.
2. Replace app-owned product identity strings with my app-specific name and bundle identifier. Do not rename upstream docs, changelog entries, or comments unless they are bundled into the app or compiled into Release.
3. Rename the design-system source prefix/path references that cause `DesignSystem/SAI` to appear in Release. Use the repo's rebrand script if available (positional args: old prefix, new prefix, mode):
`bash scripts/rebrand-design-system-prefix.sh SAI [PREFIX] --apply`
4. Verify no dead demo/showcase Swift files or bundled markdown resources still compile into the app target.
5. Run:
`bash scripts/appstore-43-audit.sh`
6. If the audit still fails, classify every remaining hit as:
- intentional upstream template/docs
- DEBUG/test-only
- third-party SDK noise
- still-blocking production/app-owned
Constraints:
- Keep useful boilerplate features unless I explicitly ask to remove them.
- Preserve DEBUG clone-and-run behavior.
- No force unwraps in new production code.
- Do not hide failures by weakening the audit script.
```
Paste this into Claude Code, Cursor, or your editor's AI chat after you've branched off `main`. It runs the heavy renaming pass and flags couplings before you submit.
```markdown theme={null}
You are auditing an iOS app for App Store Guideline 4.3 (Spam) and 4.2.6
(template apps) before submission. The app is built on SwiftAI Boilerplate
Pro; the architecture is shared with other apps, which is fine. The goal
is to remove undifferentiated product, listing, and binary fingerprints.
Do these passes in order. After each pass, print a short report and wait
for me to confirm before continuing.
Pass 1: Branding map.
- Search the repo (excluding Tests, .build, Pods, DerivedData) for any
occurrence of: "SwiftAI", "Boilerplate", "SwiftAIBoilerplatePro",
"swiftaiboilerplate.com", boilerplate support email, boilerplate
bundle ID prefix.
- Output a table: file path, line, old token, suggested replacement
using my product name "" and bundle ID "".
- Do not edit yet. Wait for me to confirm.
Pass 2: User-visible strings.
- Once I confirm, apply the renames from Pass 1 only to user-visible
strings: .strings files, Localizable, SwiftUI Text(...), accessibility
labels, push payload templates, email templates.
- Leave Swift type names, package names, and folder names alone unless
I tell you otherwise.
- Print a diff summary.
Pass 3: Dead app-target code.
- List every file in the **app target's** Compile Sources whose symbols
aren't referenced from any view that appears in the running app's
navigation graph. Include sample data, example views, and demo
flows.
- Output the candidate removal list. Do not delete; I'll review.
Pass 4: Module coupling check.
- If FeatureChat is being removed, list every reference to its public
types from outside the FeatureChat package, especially in the AI
module's clients and CompositionRoot.
- Same for Payments → FeatureSettings paywall hooks.
- Output a "blocking removals" list with file:line references.
Pass 5: App Store listing review.
- Read the screenshots referenced in /AppStoreAssets (or wherever I
point you) and the App Store description draft if I paste one.
- Tell me: does the first screenshot lead with the hero feature?
Does the description's first sentence answer "what this app does
for me"? Where does it still sound like a generic AI-chat
template?
Constraints:
- Don't invent files or symbols. If you can't find something, say so.
- Don't claim the app will pass review. Your job is to reduce the
patterns reviewers commonly cite under 4.3 / 4.2.6.
- Stop after each pass and print a short summary.
```
## If you already got a 4.3 rejection
Don't panic, and don't argue with the reviewer in the first reply.
Apple's message usually quotes the guideline number and points at one or two specific things: a screenshot, a string, a flow, or a binary similarity. Treat that as the entire problem statement, not a hint at a wider issue.
Even if the rejection mentions only one thing, re-run `bash scripts/appstore-43-audit.sh` and walk every pre-submit step. Reviewers compare binaries; if one fingerprint matched, others probably did too.
In App Store Connect → Resolution Center, write a short reply: what you changed, why your app is differentiated, and a one-paragraph plain-language description of what users do with it. Resubmit a new build with a bumped build number.
If you've done the work and still believe the rejection is wrong, you can request a phone call through App Review. Show up with your branding map, screenshots, and a clear answer to "who is this for and what makes it different." Don't argue the guideline; argue the facts.
## What a boilerplate can't guarantee
Honest version: a boilerplate can save you weeks of plumbing. It can't tell Apple your app is differentiated, because that's a judgment call about your product, your listing, and your category. The patterns above are the ones reviewers cite most often. They don't cover product-market fit, content quality, or whether your category is genuinely too crowded.
If your idea is "a chat app with a different color scheme," no checklist will save you. If your idea is a real product that happens to use a chat interface, this checklist removes the noise so reviewers can see it.
## Related
Branding, backend, and feature customization
TestFlight and App Store submission steps
Colors, themes, onboarding, and modules
Read Apple's wording directly
# Authentication Guide
Source: https://docs.swiftaiboilerplate.com/pages/guides/authentication
Unified authentication with Apple, Google, and Email via Supabase
This boilerplate provides a complete authentication solution using Supabase Auth with support for:
* **Apple Sign In** - Native iOS authentication
* **Google Sign In** - OAuth via Google
* **Email/Password** - Traditional email authentication
All authentication methods are unified through Supabase, providing a consistent user experience and automatic session management.
You can skip this guide while exploring the app in mock mode. Complete it when you are ready to connect real Supabase authentication providers.
## Features
### Unified Sign In Experience
* **Single Sign In Screen**: All authentication options on one screen
* **Social OAuth**: Apple and Google sign in via Supabase Auth
* **Email Authentication**: Sign up and sign in with email/password
* **Password Reset**: Forgot password flow via email
### Session Persistence
✅ **Users stay logged in for days, not hours** - Production-ready session management that just works
The app implements battle-tested session persistence:
1. **Keychain Storage**: Auth tokens are securely stored in iOS Keychain
2. **Automatic Session Loading**: When the app launches, sessions are restored from Keychain
3. **Expired Session Recovery**: Even if access token has expired, the app automatically refreshes using the refresh token
4. **Proactive Refresh**: Tokens are refreshed 60 seconds before expiry
5. **Retry Logic**: Failed refresh attempts retry up to 3 times with exponential backoff
### Session Behavior
| Scenario | Behavior |
| ------------------------- | ---------------------------------------------- |
| App opened after 1 hour | ✅ Silent refresh, user stays logged in |
| App opened after 3 days | ✅ Silent refresh, user stays logged in |
| App opened after 14+ days | Refresh token expired, user must sign in again |
Refresh token lifetime is configurable in your Supabase dashboard. Default is 7+ days.
### Session Lifecycle
```text theme={null}
App Launch
↓
Load Session from Keychain
↓
Access Token Valid? ──No──→ Attempt Refresh with Refresh Token
↓ Yes ↓
↓ Refresh Successful? ──No──→ Show Sign In Screen
↓ ↓ Yes
Navigate to Main Screen ←────────┘
↓
Schedule Proactive Refresh (60s before expiry)
↓
Silent background refresh
↓
Continue using app seamlessly
```
## Setup
### 1. Configure Supabase
#### Enable Social Providers
1. Go to [Supabase Dashboard](https://app.supabase.com)
2. Select your project
3. Navigate to **Authentication** → **Providers**
### Apple Sign In Setup (Step-by-Step)
In your Supabase dashboard, navigate to **Authentication** settings. This is where you'll enable Apple as a sign-in provider.
Go to **Sign In Providers** and find Apple in the list. Click on it to configure.
Toggle **Enable Sign in with Apple** to ON. This activates Apple as an authentication provider in your Supabase project.
In [Apple Developer Portal](https://developer.apple.com), go to **Certificates, Identifiers & Profiles**:
1. Select **Identifiers** → Your App ID
2. Ensure **Sign in with Apple** capability is checked
3. Click **Save**
Complete the Apple Developer Portal configuration:
* **Services ID**: Create one if needed for web callback
* **Team ID**: Your Apple Developer Team ID
* **Key ID**: Generate a new key with Sign in with Apple enabled
* **Private Key**: Download and save securely
Add these to your Supabase Apple provider settings.
In Xcode, go to your target's **Signing & Capabilities**:
1. Click **+ Capability**
2. Search for "Sign in with Apple"
3. Add it to your target
This enables the native Apple Sign In button in your app.
Apple requires apps that offer third-party login (like Google) to also support Sign in with Apple. The boilerplate has this pre-built with Supabase integration.
### Google Sign In Setup
Google Sign In uses two pieces of configuration:
* A **Web OAuth client** for Supabase, because Supabase owns the OAuth callback URL
* An **iOS OAuth client** for the native Google Sign-In SDK, because the app runs on iPhone and is identified by your Bundle ID
If Google asks for a Bundle ID, use your app's iOS bundle identifier. The boilerplate derives every target's bundle ID from a single `APP_BUNDLE_ID` in `Config/App.xcconfig` (each target sets `PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_ID)`), so set `APP_BUNDLE_ID` there or read the resolved value from Xcode target settings. If Google asks for an App Store ID, you can usually leave it blank while developing locally. Add it later after you create the app record in App Store Connect.
In your Supabase project, copy your project reference from the project URL, then build this callback URL:
```text theme={null}
https://YOUR_PROJECT_REF.supabase.co/auth/v1/callback
```
Keep this URL open. You will paste it into Google Cloud in the next step.
Open [Google Cloud Console](https://console.cloud.google.com), select or create a project, then go to **APIs & Services** -> **Credentials**.
1. Click **Create Credentials** -> **OAuth client ID**
2. Choose **Web application**
3. Name it something like `SwiftAI Supabase Web`
4. Add the Supabase callback URL under **Authorized redirect URIs**
5. Save the client
Copy the generated **Client ID** and **Client Secret**. These values go into Supabase.
In Supabase, go to **Authentication** -> **Sign In / Providers** -> **Google**.
1. Toggle **Enable Sign in with Google** on
2. Paste the **Web OAuth Client ID** from Google Cloud
3. Paste the **Web OAuth Client Secret** from Google Cloud
4. Save the provider
Back in Google Cloud **Credentials**, create a second OAuth client:
1. Click **Create Credentials** -> **OAuth client ID**
2. Choose **iOS**
3. Enter your **Bundle ID** exactly as it resolves from `APP_BUNDLE_ID` in `Config/App.xcconfig`, for example `com.yourcompany.yourapp`
4. Leave **App Store ID** blank for local development if you do not have an App Store Connect app yet
5. Save the client
Copy the generated **iOS Client ID**. This value goes into your app's `Config/Secrets.xcconfig` as `GOOGLE_CLIENT_ID`.
### 2. Configure iOS App
**Update Secrets Configuration:**
```bash theme={null}
# Config/Secrets.xcconfig
SUPABASE_URL = https://YOUR_PROJECT_REF.supabase.co
SUPABASE_ANON_KEY = YOUR_PUBLIC_ANON_KEY
# For Google Sign In (optional)
GOOGLE_CLIENT_ID = YOUR_IOS_CLIENT_ID
```
After editing `Config/Secrets.xcconfig`, run `bash scripts/update-config.sh` and rebuild the app. The generated Swift configuration is not updated automatically.
**Add Google Sign In SDK (Optional):**
Google Sign In is optional. `GoogleSignInCoordinator` is guarded by `#if canImport(GoogleSignIn)`, so the app builds and runs without the SDK — Google sign-in is simply unavailable until you add it.
To enable it, add the GoogleSignIn-iOS package to the Xcode project via **File → Add Package Dependencies**:
```text theme={null}
https://github.com/google/GoogleSignIn-iOS
```
Then add the `GoogleSignIn` product to the app target.
### Google Credential Reference
| Field | Where to Get It | Where to Put It |
| --------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Supabase callback URL | Supabase project ref | Google Cloud Web OAuth client -> Authorized redirect URIs |
| Web Client ID | Google Cloud Web OAuth client | Supabase Google provider |
| Web Client Secret | Google Cloud Web OAuth client | Supabase Google provider |
| iOS Client ID | Google Cloud iOS OAuth client | `Config/Secrets.xcconfig` as `GOOGLE_CLIENT_ID` |
| Bundle ID | `APP_BUNDLE_ID` in `Config/App.xcconfig` (or Xcode target settings) | Google Cloud iOS OAuth client |
| App Store ID | App Store Connect app record | Optional for local development; add before production if available |
### 3. Setup CompositionRoot
The boilerplate comes with auth pre-configured, but here's how it works:
```swift theme={null}
// CompositionRoot.swift
// Create auth client with social providers
let authClient = SessionManager(
httpClient: httpClient,
keychain: keychain,
apple: AppleSignInCoordinator(),
google: GoogleSignInCoordinator(clientID: googleClientID), // Optional
config: authConfig
)
```
## Usage
### Sign In Flow
The `SignInView` provides a premium, minimal authentication experience:
```swift theme={null}
SignInView(authClient: authClient)
```
**Design Philosophy:**
* Clean, Apple-like aesthetic with generous whitespace
* Native Apple button (HIG compliant) as primary CTA
* Visual hierarchy: Apple → Google → "Use email instead" link
* Premium feel with continuous corner radii and subtle borders
* Adapts to light/dark mode automatically
**Features:**
* **Apple Sign In**: Native button (HIG compliant) - primary option
* **Google Sign In**: Secondary button with subtle border
* **Email Sign In**: Shown via "Use email instead" link (opens sheet)
* **Create Account**: Accessible from email login sheet
* **Forgot Password**: Accessible from email login sheet
**Theme Integration:**
All auth screens (Sign In, Sign Up, Forgot Password) adapt to the active theme:
* Links and accents use `DSColors.accentPrimary` (warm pink in Aurora, neutral in others)
* Text colors use `DSColors.textPrimary` and `DSColors.textSecondary`
* Backgrounds and surfaces adapt to theme palette
* Icons inherit theme accent color
* No hardcoded system blue - everything theme-aware
**Customization:**
To adjust the visual design, edit `SwiftAIBoilerplatePro/AppShell/SignInView.swift`:
**Spacing & Rhythm:**
* Top space: `Spacer().frame(height: 100)` - Adjust for vertical positioning
* Between hero and buttons: `64pt` - Control breathing room
* Bottom space: `80pt` then `48pt` - Legal section padding
**Typography:**
* App title: `28pt semibold rounded` - Adjust size/weight
* Tagline: `15pt regular, 0.3 tracking, 70% opacity` - Refine elegance
* Legal text: `12pt, 55% opacity` - Keep whisper-quiet
**Colors & Effects:**
* Avatar blur: `blur(radius: 20)` on gradient circle
* Background: Subtle top-to-bottom gradient
* Google button: `surface.opacity(0.6)` with `0.5pt border`
* Link colors: Uses `accentPrimary.opacity(0.7)` for cohesion
**Animations:**
* Hero fade-in: `.easeOut(duration: 0.6)` on appear
* Buttons slide: `.delay(0.15)` for staggered entrance
* Error message: `.spring(response: 0.35)` for smooth entry
**Container Width:**
* Max width: `380pt` for button container (narrower = more refined)
### Check Authentication State
```swift theme={null}
// Check current user
let user = await authClient.currentUser()
if let user = user {
print("Signed in as: \(user.email ?? "unknown")")
} else {
print("Not signed in")
}
// Observe auth state changes
for await state in authClient.authStates() {
switch state {
case .authenticated(let user):
print("User signed in: \(user.id)")
case .unauthenticated:
print("User signed out")
case .refreshing:
// Show loading UI while recovering expired session
print("Refreshing session...")
}
}
```
### Sign Out
```swift theme={null}
try await authClient.signOut()
```
This will:
1. Revoke the access token on the server
2. Clear tokens from Keychain
3. Clear in-memory session
4. Navigate to sign-in screen
## How It Works
### Apple Sign In Flow
1. User taps "Continue with Apple"
2. System shows Apple Sign In sheet
3. User authenticates with Face ID/Touch ID
4. App receives Apple ID token + nonce
5. Token is exchanged with Supabase for session
6. Session stored in Keychain
7. User navigated to main screen
```swift theme={null}
// Internal flow
let (idToken, nonce) = try await apple.requestIDToken(...)
let session = try await api.exchangeAppleIDToken(idToken: idToken, nonce: nonce)
try await persistSession(session)
```
### Google Sign In Flow
1. User taps "Continue with Google"
2. Google Sign In SDK presents OAuth flow
3. User selects Google account
4. App receives Google ID token
5. Token is exchanged with Supabase for session
6. Session stored in Keychain
7. User navigated to main screen
```swift theme={null}
// Internal flow
let idToken = try await google.requestIDToken()
let session = try await api.exchangeGoogleIDToken(idToken: idToken)
try await persistSession(session)
```
### Email Sign In Flow
1. User enters email and password
2. Credentials sent to Supabase
3. Supabase validates and returns session
4. Session stored in Keychain
5. User navigated to main screen
```swift theme={null}
// Internal flow
let session = try await api.signInWithEmail(email: email, password: password)
try await persistSession(session)
```
## Session Persistence Details
### How Sessions Are Stored
Sessions are stored in iOS Keychain with the following keys:
```text theme={null}
auth_access_token - JWT access token
auth_refresh_token - Long-lived refresh token
auth_expires_at - Token expiration timestamp
auth_user_json - User profile data
```
Security:
* Uses `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`
* Data is encrypted by iOS
* Survives app updates
* Deleted on app uninstall
### Automatic Session Loading
On app launch (`loadInitialSession()`):
1. **Load from Keychain**: Attempts to load stored session
2. **Validate Expiry**: Checks if access token is expired
3. **Attempt Refresh**: If expired, tries to refresh using the refresh token (up to 3 attempts)
4. **Recover or Clear**: Restores session if refresh succeeds, clears if refresh token is also expired
5. **Emit State**: Notifies observers of auth state (`.refreshing` during recovery)
```swift theme={null}
// SessionManager+Persistence.swift
func loadInitialSession() async {
do {
if let session = try loadSession() {
if Date.now > session.expiresAt {
// Access token expired - try the refresh token before
// signing the user out (it usually outlives the access token)
await attemptSessionRefresh(with: session)
return
}
// Session valid, restore it
currentSession = session
emit(.authenticated(session.user))
// Schedule proactive refresh (or refresh now if due soon)
if session.needsRefresh {
Task { try? await refreshIfNeeded() }
} else {
scheduleRefresh(for: session)
}
}
} catch {
try? clearSession()
}
}
```
The `.refreshing` state allows you to show a loading indicator while the session is being recovered, providing a smooth user experience.
### Token Refresh Strategy
The app uses **proactive refresh** and **reactive recovery** to ensure users stay logged in:
**Proactive Refresh (Before Expiry):**
1. **Schedule Refresh**: 60 seconds before token expiry
2. **Silent Background Execution**: User never sees a loading state
3. **Token Rotation**: Handles Supabase refresh token rotation
**Reactive Recovery (After Expiry):**
1. **Detect Expired Session**: On app launch, check if access token expired
2. **Attempt Refresh**: Use refresh token to get new access token
3. **Retry Logic**: Up to 3 attempts with exponential backoff
4. **Emit `.refreshing` State**: Show loading UI if needed
```swift theme={null}
// Proactive: Scheduled 60 seconds before expiry
let refreshTime = session.expiresAt.addingTimeInterval(-60)
let delay = max(0, refreshTime.timeIntervalSinceNow)
refreshTask = Task {
try await sleeper.sleep(for: delay)
if !Task.isCancelled {
try await refreshIfNeeded()
}
}
// Reactive: Recover an expired session on app launch
func attemptSessionRefresh(with expiredSession: AuthSession) async {
currentSession = expiredSession
emit(.refreshing)
for attempt in 1...3 {
do {
let newSession = try await api.refresh(refreshToken: expiredSession.refreshToken)
try await persistSession(newSession)
currentSession = newSession
emit(.authenticated(newSession.user))
scheduleRefresh(for: newSession)
return
} catch {
if attempt < 3 {
try? await sleepBeforeRetry(attempt: attempt)
}
}
}
// Refresh token also invalid - clear and require sign-in
try? clearSession()
currentSession = nil
emit(.unauthenticated)
}
```
**Result**: Users stay logged in for days (until refresh token expires), not just 1 hour (access token lifetime).
## Navigation Based on Auth State
The `LaunchRouter` automatically handles navigation:
```swift theme={null}
// LaunchRouter observes auth state
for await state in authClient.authStates() {
switch state {
case .authenticated:
destination = .main // Show main app
case .unauthenticated:
destination = .signIn // Show sign in
case .refreshing:
// Keep current screen, just refreshing token
}
}
```
This means:
* ✅ Users automatically navigate to main screen when signed in
* ✅ Users automatically navigate to sign in when signed out
* ✅ No manual navigation needed
* ✅ Works for all auth methods (Apple, Google, Email)
## Best Practices
### Always Check Auth State
```swift theme={null}
// Don't store user state yourself
// ❌ Bad
@State private var isSignedIn = false
// ✅ Good - observe auth state
for await state in authClient.authStates() {
// React to state changes
}
```
### Handle Sign Out Gracefully
```swift theme={null}
func signOut() {
Task {
do {
try await authClient.signOut()
// Navigation handled automatically by LaunchRouter
} catch {
// Show error to user
errorMessage = "Failed to sign out: \(error.localizedDescription)"
}
}
}
```
### Use Session Tokens for API Calls
When the `HTTPClient` is built with an `AuthInterceptor`, the current access token is attached to every request automatically — call sites never touch the token:
```swift theme={null}
// AuthInterceptor adds `Authorization: Bearer ` if the
// header isn't already set, sourcing the token from a TokenProvider.
let request = HTTPRequest(path: "/rest/v1/profiles", method: .get)
let response = try await httpClient.send(request)
let profile = try JSONDecoder().decode(UserProfile.self, from: response.data)
```
## Troubleshooting
### Users Have to Sign In Every Time
This should be rare with the new session recovery. Check:
1. Keychain is accessible (not disabled in Xcode capabilities)
2. `loadInitialSession()` is being called
3. No errors in `SessionManager` initialization
4. Check logs for refresh attempts and failures
5. Verify refresh token hasn't expired (7+ days of inactivity)
With proper session recovery, users only need to sign in again if their refresh token expires (typically 7+ days of not opening the app).
### Google Sign In Not Working
1. Verify `GOOGLE_CLIENT_ID` is set to the **iOS OAuth Client ID**, not the Web Client ID.
2. Verify the Supabase Google provider uses the **Web OAuth Client ID** and **Web OAuth Client Secret**.
3. Ensure the Google Cloud Web OAuth client has this exact authorized redirect URI: `https://YOUR_PROJECT_REF.supabase.co/auth/v1/callback`.
4. Ensure the Google Cloud iOS OAuth client Bundle ID exactly matches the bundle ID resolved from `APP_BUNDLE_ID` in `Config/App.xcconfig`.
5. If you changed `Secrets.xcconfig`, run `bash scripts/update-config.sh`, clean, and rebuild.
6. Verify GoogleSignIn SDK is installed.
7. For local development, do not block on App Store ID. Add it later after creating the app in App Store Connect.
### Apple Sign In Fails
1. **Supabase Configuration**: Verify Apple provider is enabled in Supabase Authentication → Providers
2. **Apple Developer Portal**: Check that Sign in with Apple capability is enabled on your App ID
3. **Xcode Capability**: Ensure "Sign in with Apple" capability is added in Signing & Capabilities
4. **Credentials**: Verify Team ID, Key ID, and Private Key are correct in Supabase
5. **Bundle ID**: Check bundle ID matches between Xcode, App Store Connect, and Supabase
### Token Refresh Fails
Check logs for:
* Network connectivity issues
* Supabase service status
* Invalid refresh token (forces re-authentication)
* Too many concurrent refresh attempts
## Security Considerations
1. **Tokens in Keychain**: Never store tokens in UserDefaults or files
2. **HTTPS Only**: All auth requests use HTTPS
3. **Token Expiry**: Access tokens expire (default: 1 hour), but sessions persist via refresh tokens
4. **Refresh Token Lifetime**: Configurable in Supabase dashboard (default: 7+ days)
5. **Refresh Rotation**: Supabase rotates refresh tokens for security
6. **Server-Side Validation**: Always validate tokens server-side
7. **Detailed Logging**: Auth events are logged for debugging (tokens are never logged)
## Next Steps
Official Supabase authentication documentation
Apple's official Sign in with Apple documentation
Google Identity Platform documentation
Technical documentation for the Auth package
# Building Your App
Source: https://docs.swiftaiboilerplate.com/pages/guides/building-your-app
Complete step-by-step customization guide from boilerplate to App Store
**Complete guide:** This page is the web version. For the deeper prompt library, see `docs/BUILDING_YOUR_APP.md` in your project.
**Before you submit:** Run the [App Store 4.3 hardening checklist](/pages/guides/app-store-4-3-hardening). Branded boilerplates get flagged when the binary, listing, and screenshots still look like the template. Five passes, one prompt, \~1 hour.
## What This Guide Covers
This is your **complete roadmap** from boilerplate to App Store:
Name, icon, colors, onboarding
Supabase, RevenueCat, API keys
Customize chat, home, profile, settings
Privacy, terms, compliance
Run tests, fix bugs
App Store submission
## Two Ways to Use This Guide
**Step-by-step instructions** with exact file paths:
* What to change
* Where to find it
* How to verify it works
* What to test
**Time:** Detailed, thorough approach
**Ready-to-use LLM prompts** for Cursor/Claude:
* Copy prompt
* Paste in AI assistant
* AI makes the changes
* You review and test
**Time:** 3-5x faster
Prefer a guided, automated path? The **App Generator** is a separate, standalone macOS product (coming soon) that drives your own coding-agent CLI to scaffold a differentiated, App Store Guideline 4.3-safe app from this template. It is not bundled in the boilerplate. See [license & pricing](/pages/reference/license) for details.
## Quick Examples
### Change App Name
**Manual:**
1. Set the user-facing name in `BrandConfig.appDisplayName` (`Packages/DesignSystem/Sources/DesignSystem/Tokens/BrandConfig.swift`)
2. Set the app identity in `APP_BUNDLE_ID` in `Config/App.xcconfig` — every target's bundle ID and app-group entitlements derive from this single value
**AI Prompt:**
```text theme={null}
Change appDisplayName to "MyAwesomeApp" and APP_BUNDLE_ID to
"com.mycompany.myawesomeapp". Update all user-facing text.
```
### Customize Colors
**Manual:**
1. Open `SwiftAIBoilerplatePro/Resources/DesignSystemColors.xcassets` and edit `Primary.colorset`
2. Edit the accent color in `SwiftAIBoilerplatePro/Assets.xcassets/AccentColor.colorset`
3. Test in light/dark mode
**AI Prompt:**
```text theme={null}
Change primary color to #6C5CE7 (purple) and accent to #00D4AA (teal).
Update Primary.colorset in SwiftAIBoilerplatePro/Resources/DesignSystemColors.xcassets
and AccentColor.colorset in SwiftAIBoilerplatePro/Assets.xcassets, both for light and dark.
```
## Complete Guide Contents
### Initial Setup
* Clone and run
* Change branding
* Customize onboarding
### Backend
* Supabase deployment
* RevenueCat setup
* API configuration
### Feature Customization
* Home screen
* Chat experience
* Profile
* Settings
* Add new features
### Legal & Compliance
* Privacy policy
* Terms of service
* App Store requirements
### Testing & Launch
* Run test suite
* TestFlight beta
* App Store submission
* Production checklist
### LLM Prompts Library
**Ready-to-use prompts** throughout the guide for:
* Branding changes
* UI customization
* Feature additions
* Backend setup
* Troubleshooting
## Quick Start Path
Clone → Run → Understand architecture
Name → Icon → Colors → Onboarding
Supabase → RevenueCat → Test real auth
Home → Chat → Your unique features
Update policies → Add required disclosures
Test → App Store assets → [4.3 hardening](/pages/guides/app-store-4-3-hardening) → Submit
## View Complete Guide
Comprehensive guide with manual steps AND LLM prompts
## Related Resources
Backend deployment
Subscription configuration
AI coding guidelines for assistants working in your project
System design
## Common Tasks
* App name and bundle ID
* App icon
* Primary and accent colors
* Onboarding content
* Create SwiftData model
* Create repository
* Create ViewModel
* Create View
* Wire up in CompositionRoot
* Edit hero section
* Change feature cards
* Modify quick actions
* Update legal docs
* Prepare App Store assets
* Run full test suite
* Archive and submit
## Success Tips
**For Manual Development:**
* Follow file paths exactly
* Test after each change
* Commit to git frequently
* Use ⌘⇧F to search code
**For AI-Assisted Development:**
* Always reference relevant docs in prompts
* Review AI changes before running
* Ask AI to verify changes compile
* One feature at a time
## Need Help?
The [full guide](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/blob/main/docs/BUILDING_YOUR_APP.md) adds a troubleshooting section, common pitfalls, per-phase verification steps, and pro tips. For the test workflow, see the [Testing guide](/pages/guides/testing) — one `Boilerplate.xctestplan` run covers \~598 tests across 12 package test targets plus the app test suites.
# Chat Sync Setup
Source: https://docs.swiftaiboilerplate.com/pages/guides/chat-sync
Enable cross-device conversation synchronization
Chat sync is **optional and off by default** — chat history stays local-only in SwiftData until you enable it. The complete setup guide lives at `docs/integrations/ChatSync.md` in your project.
## What Is Chat Sync?
Writes go to the local database first
Syncs to cloud in background
Access from all devices
Enable only when needed
## Quick Overview
Create Supabase tables:
```bash theme={null}
# In Supabase Dashboard SQL Editor
# Run: supabase/migrations/20241016000000_chat_sync.sql
```
Flip the `chatSyncEnabled` flag in `SwiftAIBoilerplatePro/Composition/FeatureFlags.swift`. By default it is `false` in DEBUG and reads the `CHAT_SYNC_ENABLED` environment variable in Release:
```swift theme={null}
public static var chatSyncEnabled: Bool {
#if DEBUG
return false // Off by default in debug — change to true to test locally
#else
return ProcessInfo.processInfo.environment["CHAT_SYNC_ENABLED"] == "true"
#endif
}
```
Both Supabase repositories ship commented out until the Supabase dependency is added. Uncomment them in the Storage package:
* `Packages/Storage/Sources/Storage/Repositories/SupabaseConversationRepository.swift`
* `Packages/Storage/Sources/Storage/Repositories/SupabaseMessageRepository.swift`
In `SwiftAIBoilerplatePro/Composition/CompositionRoot.swift`, the chat-sync branch currently falls back to the local repositories (a `// TODO: wire Supabase remote repos once a signed-in user id is available` marks the spot). Replace that with the hybrid repositories once you have a signed-in user id:
```swift theme={null}
self.conversationRepository = HybridConversationRepository(
local: localConversationRepo,
remote: remoteConversationRepo
)
self.messageRepository = HybridMessageRepository(
local: localMessageRepo,
remote: remoteMessageRepo
)
```
## How It Works
```text theme={null}
Write Flow:
User sends message → Local SwiftData (instant) → Background sync to Supabase
Read Flow:
App opens → Show local data (instant) → Background pull from Supabase → Update UI
```
`HybridConversationRepository` and `HybridMessageRepository` write to the local SwiftData store first and sync to the remote Supabase repository in a background `Task`. If the remote write fails, the local save still succeeds, so the app keeps working offline.
## Prerequisites
* Supabase project configured (see [Supabase Setup](/pages/guides/supabase-setup))
* Supabase dependency added to the Storage package
* User authenticated
## What You Get
* Conversations and messages sync across devices
* Offline-first behavior preserved (local writes never block on the network)
* Background sync that degrades gracefully when the remote is unavailable
## Complete Guide
The full `docs/integrations/ChatSync.md` guide covers the database schema, Row Level Security policies, repository implementations, the hybrid sync strategy, conflict resolution, testing, and troubleshooting.
Full chat sync guide with architecture diagrams, in your project repo
## Related Guides
Repository architecture
Another optional sync feature
Required backend
Customize features
## Need Help?
* Troubleshooting section in the full `docs/integrations/ChatSync.md` guide
* [Create an issue](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/issues)
* Check the Supabase Dashboard logs
# Crashlytics Setup
Source: https://docs.swiftaiboilerplate.com/pages/guides/crashlytics-setup
Add Firebase Crashlytics to track crashes and errors in production.
**Complete setup guide:** See `/docs/integrations/Crashlytics.md` in the project
## What is Crashlytics?
Automatic crash detection with stack traces
Non-fatal errors and custom logging
GDPR compliant with user consent toggle
Unlimited reports on Firebase free tier
Users rarely report crashes. Without crash reporting, you are flying blind. Crashlytics captures every crash with full context so you can fix issues before they tank your App Store ratings.
## Step-by-Step Setup
Navigate to the [Firebase Console](https://console.firebase.google.com) and create a new project or select an existing one.
Give your Firebase project a name. This will be your crash reporting dashboard for monitoring your iOS app.
For now, you can disable Google Analytics. You can always enable it later if needed. Crashlytics works independently.
Click **Add app** and select the iOS platform. This will start the iOS app registration process.
Enter your iOS bundle identifier. Make sure it matches exactly with your Xcode project bundle ID.
Download the `GoogleService-Info.plist` file. Add it to your Xcode project root and make sure it's added to your target.
* ✅ Check "Copy items if needed"
* ✅ Select your app target
Add the Firebase SDK to your project. We recommend using Swift Package Manager (SPM) for easier dependency management.
In Xcode, go to **File → Add Packages**. Enter the Firebase iOS SDK URL:
```text theme={null}
https://github.com/firebase/firebase-ios-sdk
```
Select the **FirebaseCrashlytics** package.
Add Firebase initialization code to your `App.swift` or `AppDelegate`:
```swift theme={null}
import Firebase
// In your App init or AppDelegate
FirebaseApp.configure()
```
In Build Settings, add `-ObjC` to **Other Linker Flags**. This is required for Firebase to work correctly with Swift.
Add a new Run Script phase in **Build Phases**:
1. Click **+** → **New Run Script Phase**
2. Name it "Upload dSYMs to Crashlytics"
3. Add the upload script (see Firebase docs)
Add the required input files to the run script phase:
```text theme={null}
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}
$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)
```
Verify your run script phase is configured correctly with both the script and input files. This ensures crash reports are properly symbolicated.
In **Build Settings**, set **Debug Information Format** to `DWARF with dSYM File` for Release builds. This generates the debug symbols needed for crash reports.
Add a test crash button to verify Crashlytics is working:
```swift theme={null}
#if DEBUG
Button("Test Crash") {
fatalError("Test crash for Firebase")
}
#endif
```
Run your app on a **real device** (not simulator) and trigger the test crash. Relaunch the app to send the crash report to Firebase.
Crashlytics does not work reliably in the simulator. Always test on a real device.
Check your Firebase Crashlytics dashboard. You should see the test crash appear (may take a few minutes).
Remember to **remove the test crash code** before submitting to the App Store!
## How It Works
### Automatic Integration
The boilerplate includes a **CrashReporter protocol** that automatically switches between Firebase Crashlytics and NoOp based on availability:
```swift theme={null}
// From CompositionRoot.swift (automatic)
// checkCrashlyticsAvailable() returns true when GoogleService-Info.plist is bundled
if FeatureFlags.crashlyticsEnabled && Self.checkCrashlyticsAvailable() {
self.crashReporter = CrashlyticsCrashReporter() // Real tracking
} else {
self.crashReporter = NoOpCrashReporter() // No tracking
}
```
`crashlyticsEnabled` is off in DEBUG and reads the `CRASHLYTICS_ENABLED` environment variable in Release (`FeatureFlags.swift`). Add the plist and flip the flag — no other code changes needed.
### User Privacy
Users control crash reporting in **Settings → Share Diagnostics**:
* ✅ ON → Crashes sent to Firebase
* ❌ OFF → No data sent
**GDPR Compliant:**
* Never sends email addresses
* User has opt-out control
* Clear privacy disclosure
## What Gets Tracked
### Automatically
* Crash reports with stack traces
* Device info (model, iOS version)
* App version
* User ID (anonymized, no email)
* Session info
### Custom (Optional)
* Non-fatal errors
* Custom logs/breadcrumbs
* User attributes (subscription tier, theme, etc.)
## Viewing Reports
**Firebase Console:**
1. Go to Firebase Console
2. Select your project
3. Crashlytics (in sidebar)
4. View crashes, errors, logs
**Reports include:**
* Stack traces
* Device information
* User ID
* App version
* Custom logs
## Cost
**Firebase Free Tier:**
* ✅ Unlimited crash reports
* ✅ 30-day retention
* ✅ Basic analytics
* ✅ Enough for most apps
**Blaze Plan (optional):**
* Extended retention
* More features
* Pay as you go (very low cost)
## Troubleshooting
* Make sure you tested on a **real device**, not simulator
* Relaunch the app after the crash to send the report
* Wait a few minutes for the report to appear
* Check that `GoogleService-Info.plist` is in your target
* Verify dSYM upload script is running
* Check that Debug Information Format is set to `DWARF with dSYM File`
* Ensure input files are configured correctly
* Make sure `-ObjC` is in Other Linker Flags
* Clean build folder (⌘⇧K) and rebuild
* Check that you only added FirebaseCrashlytics package
## Related Guides
See complete details in `/docs/integrations/Crashlytics.md`
Configure in `Composition/FeatureFlags.swift`
User consent toggle
Update `Resources/privacy.md` to mention crash reporting
## Need Help?
The boilerplate handles all the integration automatically. You just need to:
1. Add `GoogleService-Info.plist`
2. Enable feature flag
3. Build and run
If you have issues, check the troubleshooting section above or in `/docs/integrations/Crashlytics.md`.
# Customization Recipes
Source: https://docs.swiftaiboilerplate.com/pages/guides/customization
Common customization patterns and examples
For complete customization guide with LLM prompts, see [Building Your App](/pages/guides/building-your-app)
**Shipping a white-labeled version?** Customizing colors and onboarding isn't enough on its own. Before you submit, run the [App Store 4.3 hardening checklist](/pages/guides/app-store-4-3-hardening) to clear binary fingerprints, branding leftovers, and listing copy that still reads like a template.
## Branding
### Change App Colors
**What to customize:** The design tokens read their colors from the DesignSystem package's own asset catalog via `Bundle.module`:
```text theme={null}
Packages/DesignSystem/Sources/DesignSystem/Resources/Assets.xcassets/
├── AccentPrimary.colorset # DSColors.accentPrimary / .primary
├── AccentSecondary.colorset
├── Background.colorset
├── Surface.colorset
├── SurfaceElevated.colorset
├── TextPrimary.colorset
├── TextSecondary.colorset
├── ChipBackground.colorset
├── Danger.colorset / Success.colorset / Warning.colorset
└── ...
```
**Steps:**
1. Open the color set in Xcode
2. Change Light Appearance color
3. Change Dark Appearance color
4. Build → colors update everywhere
**All components use design tokens** - change once, applies everywhere. Note: the `system`/`light`/`dark` themes read these assets, while the `aurora` and `obsidian` themes use hard-coded values in `DSColors.swift`, so customizing those requires editing the matching `switch activeTheme` branches.
### Replace App Icon
**Steps:**
1. Create 1024×1024 PNG
2. Drag to `SwiftAIBoilerplatePro/Assets.xcassets/AppIcon.appiconset`
3. Xcode generates all required sizes
4. Build and verify
### Customize Onboarding
**File:** `SwiftAIBoilerplatePro/AppShell/OnboardingPage.swift`
**Change the `defaultPages` array:**
```swift theme={null}
static let defaultPages: [OnboardingPage] = [
OnboardingPage(
title: "Your Title",
description: "Your description",
systemImage: "sparkles", // Any SF Symbol
accentColor: "blue" // Hex or system color name
),
// Add more pages...
]
```
## UI Customization
### Add Home Screen Feature Card
**File:** `SwiftAIBoilerplatePro/AppShell/HomeContent.swift`
Add to `HomeContent.FeatureItem.defaults` (or pass your own `featuredItems` into `HomeContent`):
```swift theme={null}
public static let defaults: [HomeContent.FeatureItem] = [
// ... existing cards
HomeContent.FeatureItem(
title: "Your Feature",
description: "Feature description",
systemImage: "bookmark.fill",
accentColor: "blue"
),
]
```
### Change Chat Bubble Colors
Bubble colors are derived from design tokens, not from dedicated bubble color sets. In `DSColors.swift`:
* `bubbleUserOrFallback` → `accentPrimary` (the `AccentPrimary.colorset` for system/light/dark)
* `bubbleAssistantOrFallback` → `surface` (a system color, `Color(.secondarySystemBackground)`, for system/light/dark)
Change the user bubble by editing `AccentPrimary.colorset` in `Packages/DesignSystem/Sources/DesignSystem/Resources/Assets.xcassets/`. The assistant bubble follows the system surface color, so adjust the `surface` token's `switch activeTheme` branch in `DSColors.swift` to override it.
### Add Custom Theme
In the DesignSystem package catalog (`Packages/DesignSystem/Sources/DesignSystem/Resources/Assets.xcassets`), create color sets for any semantic colors your theme needs.
In `SettingsDTO.swift`, add a case to the string-backed `Theme` enum (used by the settings UI):
```swift theme={null}
public enum Theme: String, Sendable, Equatable, CaseIterable {
case system, light, dark, aurora, obsidian
case myTheme // Add this
}
```
In `DSColors.swift`, add a `case .myTheme` to the internal `ThemePalette` enum and to each token `switch activeTheme` block.
The appearance picker in `SettingsAppearanceSection.swift` iterates `SettingsDTO.Theme.allCases`, so the new case appears automatically once the enum and mappings are in place.
## Feature Customization
### Add Custom AI Persona
**File:** `Packages/FeatureChat/Sources/FeatureChat/ViewModels/ChatViewModel.swift`
`LLMMessage` lives in `FeatureChat` and its `role` is a `String` (`"user" | "assistant" | "system"`):
```swift theme={null}
// Inside ChatViewModel.send() (the real entry point is `public func send() async`),
// build the prompt list with a persona system message:
let systemPrompt = LLMMessage(
role: "system",
content: """
You are a professional Swift developer.
Provide clear, concise answers with code examples.
"""
)
let messages = [systemPrompt] + history // `history` mapped to [LLMMessage]
// Pass `messages` to llmClient when streaming
```
The server-side AI Edge Function (`supabase/functions/ai/index.ts`) injects its own fixed `SYSTEM_PROMPT` and clients may only send `user`/`assistant` roles, so a client-supplied system message is for local/echo flows. To change the deployed persona, edit `SYSTEM_PROMPT` in the function and redeploy.
### Add New SwiftData Model
```swift theme={null}
@Model
class YourModel {
@Attribute(.unique) var id: UUID
var property: String
var createdAt: Date
}
```
```swift theme={null}
struct YourModelDTO: Identifiable, Sendable {
let id: UUID
let property: String
init(_ model: YourModel) {
self.id = model.id
self.property = model.property
}
}
```
Protocol + implementation following existing patterns
In `SwiftAIBoilerplatePro/Composition/CompositionRoot.swift`, add your model to the `Schema([Conversation.self, Message.self, Settings.self])` array.
### Add Custom Setting
In `Packages/Storage/Sources/Storage/Models/Settings.swift`:
```swift theme={null}
@Model
public final class Settings {
public var theme: String // "system" | "light" | "dark"
public var yourSetting: Bool = false // Add this
}
```
```swift theme={null}
@Observable
class SettingsViewModel {
var yourSetting: Bool
func toggleYourSetting(_ enabled: Bool) async {
// Save to repository
}
}
```
```swift theme={null}
Toggle("Your Setting", isOn: $viewModel.yourSetting)
.onChange(of: viewModel.yourSetting) {
Task { await viewModel.toggleYourSetting($0) }
}
```
## Backend Customization
### Change AI Model
**File:** `supabase/functions/ai/index.ts`
For cost safety, the function validates the requested model against an allowlist (security rule BL-003). To allow a different OpenRouter model, add it to `ALLOWED_MODELS`:
```typescript theme={null}
// Only allow-listed models are accepted; everything else is rejected with 400.
const ALLOWED_MODELS = [
'openai/gpt-3.5-turbo',
'openai/gpt-4o-mini',
'anthropic/claude-3-5-sonnet', // Add the models you want to permit
]
```
The request body's `model` defaults to `openai/gpt-3.5-turbo` and must be in this list. The server-side `SYSTEM_PROMPT` and role allowlist (`user`/`assistant`) are also enforced here.
**Deploy:**
```bash theme={null}
supabase functions deploy ai
```
### Add Usage Limits
`PaymentsClient.currentState()` and `state.isSubscribed` are real APIs; the per-day count below is illustrative — back it with your own repository query.
```swift theme={null}
func canSendMessage() async -> Bool {
let state = await paymentsClient.currentState()
if state.isSubscribed {
return true // Unlimited for pro
} else {
let count = await messageRepository.todayCount() // your own query
return count < 20 // Free tier limit
}
}
```
## Module Customization
Each module has detailed customization examples:
Custom errors, log categories
Custom requests, interceptors
New models, repositories
Social providers, custom fields
New tiers, usage limits
Models, personas, prompts
UI styles, actions, search
New settings, paywall
Themes, components, tokens
## Pro Tips
**Test after each change** - Don't stack multiple changes without testing
**Use design tokens** - Never hardcode colors or spacing
**Follow existing patterns** - Look for similar features first
**Keep files ≤ 300 lines** - Extract components when needed
## Need More Help?
Step-by-step guide with LLM prompts
Detailed technical docs
AI coding guidelines
System design
# Deployment Guide
Source: https://docs.swiftaiboilerplate.com/pages/guides/deployment
Ship your app to TestFlight and the App Store with OpenRouter AI integration
Find complete deployment steps in your project [Building Your App](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/blob/main/docs/BUILDING_YOUR_APP.md)
## OpenRouter Setup
Before deploying, you need to set up the AI proxy through Supabase Edge Functions. This keeps your API keys secure on the server.
Never bundle API keys in your iOS app. Keys can be extracted from the binary. Our architecture routes all AI requests through Supabase Edge Functions.
Sign up or sign in to [OpenRouter](https://openrouter.ai):
1. Create your first project
2. Generate your API key
This is a **private key**. We will deploy it directly to our Supabase Edge Function and never bundle it with our app or push it to git.
Set your OpenRouter key as a Supabase secret:
```bash theme={null}
supabase secrets set OPENROUTER_API_KEY=sk-or-your-openrouter-key
```
This keeps your API key secure on the server side.
Deploy the AI Edge Function to Supabase:
```bash theme={null}
supabase functions deploy ai
```
The boilerplate includes a ready-to-deploy Edge Function that handles all AI API calls securely.
Verify your function is deployed:
1. Go to **Supabase Dashboard** → **Edge Functions**
2. See your `ai` function listed
3. Note down the function URL
Open `Config/Secrets.xcconfig` in Xcode and set the base URL plus path. The function name (`ai`) is the `PROXY_PATH`, not part of `PROXY_BASE_URL`:
```bash theme={null}
PROXY_BASE_URL = https://your-project-ref.supabase.co/functions/v1
PROXY_PATH = /ai
```
Then run the update script:
```bash theme={null}
bash scripts/update-config.sh
```
## Deployment Phases
* Run the full test suite (\~598 tests across 12 package test targets + the app test suites, one `Boilerplate.xctestplan` run)
* Fix all linter warnings
* Test on real devices
* Update legal documents
* Archive build
* Upload to App Store Connect
* Add internal testers
* Gather feedback
* Prepare metadata
* Create screenshots
* Submit for review
* Monitor status
* Configure production backends
* Monitor crashes
* Respond to reviews
* Plan updates
## Pre-Deployment Checklist
* [ ] All tests pass (⌘U)
* [ ] No warnings in build
* [ ] Tested on real device
* [ ] Performance is smooth
* [ ] No crashes in common flows
* [ ] Real backends configured (not mock)
* [ ] API keys in `Config/Secrets.xcconfig`
* [ ] Bundle ID matches App Store Connect
* [ ] Version number set (e.g., 1.0.0)
* [ ] Build number set (start at 1)
* [ ] Privacy policy updated
* [ ] Terms of service updated
* [ ] Subscription terms updated
* [ ] Privacy policy URL works
* [ ] Terms URL works
* [ ] App icon (1024×1024)
* [ ] Screenshots for all sizes
* [ ] App description written
* [ ] Keywords added
* [ ] Support URL set
## TestFlight Deployment
### 1. Archive Build
```text theme={null}
# In Xcode
Product → Archive
# Wait for archive to complete
# Organizer window opens automatically
```
### 2. Validate Archive
```text theme={null}
# In Organizer
1. Select your archive
2. Click "Validate App"
3. Choose options:
- Upload symbols: Yes
- Manage version: No
4. Click "Validate"
5. Fix any errors
```
### 3. Upload to App Store
```text theme={null}
# In Organizer
1. Click "Distribute App"
2. Choose "App Store Connect"
3. Next → Upload
4. Wait for processing (~5-10 minutes)
```
### 4. Add Testers
**Internal Testers (up to 100):**
* App Store Connect → TestFlight → Internal Testing
* Add team members
* They receive email invite
* Install via TestFlight app
**External Testers (up to 10,000):**
* Create group: "Beta Testers"
* Add email addresses
* Requires beta review (1-2 days)
* Public link available
### 5. Test Thoroughly
* [ ] Sign up flow
* [ ] Chat functionality
* [ ] Subscription purchases (sandbox)
* [ ] Settings and preferences
* [ ] Performance on various devices
## App Store Submission
**Run [App Store 4.3 hardening](/pages/guides/app-store-4-3-hardening) before this step.** It covers the binary `strings` audit, branding map, dead-code removal, and screenshot ordering. The most common rejection reason for boilerplate-based apps is undifferentiated product, not a metadata typo.
### 1. Prepare Metadata
**In App Store Connect:**
```text theme={null}
App Information:
- Name: Your App Name
- Subtitle: Short description
- Privacy Policy URL: https://yoursite.com/privacy
- Category: Productivity (or your category)
- Content Rights: Does not use third-party content
```
### 2. Create Screenshots
**Required sizes:**
* 6.7" (iPhone 15 Pro Max): 1290 × 2796
* 6.5" (older Pro Max): 1284 × 2778
* 5.5" (older Plus): 1242 × 2208
* 12.9" iPad Pro: 2048 × 2732 (if supporting iPad)
**Tips:**
* 4-10 screenshots per size
* Show key features
* Add text overlays
* First screenshot most important
### 3. Write Description
**Template:**
```markdown theme={null}
[Your App] is an AI-powered [category] that helps you [benefit].
FEATURES:
• AI Chat - Get instant answers
• Smart Conversations - History saved
• Premium Themes - Beautiful UI
• Cross-Device Sync - Access everywhere
PERFECT FOR:
• [Target audience 1]
• [Target audience 2]
PREMIUM FEATURES:
• Unlimited messages
• All AI models
• Priority support
Privacy Policy: [URL]
Terms: [URL]
```
### 4. Configure App Privacy
Answer privacy questionnaire:
**Data collected:**
* Contact Info: Email (for account)
* Identifiers: User ID (for backend)
* Usage Data: App interactions (if analytics)
* User Content: Chat messages
For each: Linked to identity, Used for app functionality
### 5. Submit for Review
```text theme={null}
# In App Store Connect
1. App Store tab → Version
2. Add screenshots
3. Add description
4. Select build
5. Submit for Review
```
**Review time:** 24-48 hours typically
## Production Configuration
### Backend Setup
1. Use production Supabase project
2. Verify Edge Function deployed (`supabase functions deploy ai`)
3. Set production OpenRouter key (`supabase secrets set OPENROUTER_API_KEY=...`)
4. Monitor API usage in Supabase dashboard
1. Switch to **production API key** in `Secrets.xcconfig`
2. Verify App Store Connect products are approved
3. Map production products in RevenueCat offerings
4. Test a real purchase before launch
1. Add credits to your OpenRouter account
2. Set up usage alerts and spending limits
3. Monitor token usage in dashboard
4. Consider rate limiting in Edge Function
### Final Verification
```swift theme={null}
// In SwiftAIBoilerplatePro/Composition/FeatureFlags.swift
// Crashlytics is opt-in via env var, even in Release builds.
public static var crashlyticsEnabled: Bool {
#if DEBUG
return false // Off by default in debug
#else
return ProcessInfo.processInfo.environment["CRASHLYTICS_ENABLED"] == "true"
#endif
}
```
To turn Crashlytics on in production, set the `CRASHLYTICS_ENABLED=true` environment variable in your Release scheme (and follow the [Crashlytics setup guide](/pages/guides/crashlytics-setup)). Then verify your production secrets:
```bash theme={null}
# In Config/Secrets.xcconfig - Verify production URLs
SUPABASE_URL = https://production-project.supabase.co
REVENUECAT_API_KEY = appl_PRODUCTION_KEY
```
## Post-Launch
### Monitor
* [ ] Crashlytics dashboard (if enabled)
* [ ] App Store Connect analytics
* [ ] RevenueCat subscription metrics
* [ ] Supabase API usage
* [ ] User reviews and ratings
### Respond
* [ ] Reply to user reviews
* [ ] Fix critical bugs immediately
* [ ] Plan feature updates
* [ ] Gather user feedback
### Update Regularly
**Recommended cadence:**
* Bug fixes: As needed (1-3 days)
* Minor updates: Every 2-4 weeks
* Major features: Every 2-3 months
## Complete Guide
Find complete deployment checklist in your project
## Related Resources
Run full test suite
Production backend
Production subscriptions
Complete customization
## Troubleshooting
* Clean build (⌘⇧K)
* Check signing certificates
* Verify provisioning profiles
* Update Xcode if needed
* Read rejection carefully
* Fix specific issues mentioned
* Update metadata if needed
* Resubmit with explanation
* If the rejection cites Guideline 4.3 or 4.2.6, see the [4.3 hardening recovery steps](/pages/guides/app-store-4-3-hardening#if-you-already-got-a-43-rejection)
* Check Crashlytics logs
* Reproduce locally
* Fix and submit hotfix
* Expedited review if critical
# OneSignal Push Notifications
Source: https://docs.swiftaiboilerplate.com/pages/guides/onesignal-setup
Add push notifications to your app with OneSignal SDK
**Complete setup guide:** See `/docs/integrations/OneSignal.md` in the project
## What is OneSignal?
Send push notifications to engage users
Images, buttons, badges, and custom sounds
Confirmed delivery and engagement tracking
Target users by behavior, tags, or segments
**Optional Integration**: OneSignal is completely optional. Your app works perfectly fine without it. Simply don't configure the App ID if you don't need push notifications.
## Features Included
* ✅ Push notifications via OneSignal SDK
* ✅ Rich notifications with images, buttons, and badges
* ✅ Confirmed delivery analytics
* ✅ User segmentation support
* ✅ In-app messaging capability
* ✅ Notification Service Extension pre-configured
* ✅ Graceful degradation (works without configuration)
## Prerequisites
* [OneSignal account](https://onesignal.com) (free tier available)
* Apple Developer account (for push certificate)
* App ID with Push Notifications capability enabled
## Setup Guide
OneSignal provides excellent, regularly-updated documentation for iOS SDK setup. We recommend following their official guide:
Follow OneSignal's comprehensive step-by-step guide for iOS integration
### Quick Overview
Go to [OneSignal](https://onesignal.com) and create a free account. The free tier includes unlimited push notifications for up to 10,000 subscribers.
Their guide covers:
* Adding Push Notifications capability
* Configuring Background Modes
* Setting up App Groups
* Adding Notification Service Extension
* Installing the SDK
[Open OneSignal iOS SDK Setup →](https://documentation.onesignal.com/docs/ios-sdk-setup)
After OneSignal setup, add your App ID to `Config/Secrets.xcconfig`:
```bash theme={null}
ONESIGNAL_APP_ID = YOUR_ONESIGNAL_APP_ID
```
Generate the Swift configuration:
```bash theme={null}
bash scripts/update-config.sh
```
Build and run on a **real device** (push notifications don't work in simulator):
1. Run the app
2. Accept push notification permission
3. Send a test notification from OneSignal dashboard
4. Verify notification appears on device
## How It Works
### Automatic Integration
The boilerplate automatically initializes OneSignal from `AppDelegate` when the App ID is configured:
```swift theme={null}
// AppDelegate.initializeOneSignal(launchOptions:) — runs automatically
let appId = AppConfiguration.ONESIGNAL_APP_ID
guard AppConfiguration.isConfigured("ONESIGNAL_APP_ID") else {
AppLogger.error("OneSignal App ID not configured - push notifications disabled.",
category: AppLogger.notifications)
return
}
OneSignal.initialize(appId, withLaunchOptions: launchOptions)
OneSignal.Notifications.requestPermission({ accepted in
AppLogger.info("OneSignal notification permission: \(accepted ? "granted" : "denied")",
category: AppLogger.notifications)
}, fallbackToSettings: true)
```
**No code changes needed.** Just add the App ID to your config and rebuild.
### Graceful Degradation
If OneSignal is not configured:
* App launches normally
* No SDK initialization
* No errors or crashes
* Push notification code paths are skipped
## Removing OneSignal
If you don't need push notifications:
1. **Leave ONESIGNAL\_APP\_ID empty** in `Secrets.xcconfig`
2. The SDK won't initialize
3. No push notification code will run
To completely remove:
1. Remove OneSignal package from Swift Package Manager
2. Delete the Notification Service Extension target
3. Remove OneSignal references from code
4. See `/docs/integrations/OneSignal.md` for complete removal guide
## Troubleshooting
* Test on a **real device** (not simulator)
* Verify APNs certificate/key is correct in OneSignal
* Check Push Notifications capability is enabled in Xcode
* Ensure user accepted notification permission
* Check OneSignal dashboard for delivery status
* Verify Notification Service Extension is in your target
* Check image URL is HTTPS
* Ensure image is under 10MB
* Test with a simple image first
* Verify ONESIGNAL\_APP\_ID is set in Secrets.xcconfig
* Run `bash scripts/update-config.sh`
* Check Configuration.swift has the App ID
* Clean and rebuild (⌘⇧K then ⌘B)
## Cost
**OneSignal Free Tier:**
* ✅ Unlimited push notifications
* ✅ Up to 10,000 subscribers
* ✅ Basic analytics
* ✅ Enough for most indie apps
**Paid Plans:**
* More subscribers
* Advanced analytics
* A/B testing
* Priority support
## Related Guides
Comprehensive iOS SDK setup guide
Backend configuration
Another optional integration
Ship to App Store
## Need Help?
* 📖 Check the troubleshooting section above
* 📚 See `/docs/integrations/OneSignal.md` in your project
* 💬 [Create an issue](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/issues)
* 🔍 Search [OneSignal docs](https://documentation.onesignal.com)
# Profile Photo Setup
Source: https://docs.swiftaiboilerplate.com/pages/guides/profile-photos
Store profile photos in Supabase Storage for cross-device access.
**Complete setup guide:** see `docs/integrations/ProfilePhotos.md` in your project repo.
## What Is Profile Photo Storage?
Photos stored in Supabase Storage
Same photo on all devices
\~500KB target size
Works locally if upload fails
## Quick Overview
In Supabase Dashboard:
* Storage → New Bucket
* Name: `profile-photos`
* Public: ✅ Enable
Photos are uploaded under `avatars/{userId}_{timestamp}.jpg`. Add these policies (the full setup guide also adds matching UPDATE and DELETE policies):
```sql theme={null}
-- Users can upload their own photos
CREATE POLICY "Users upload own" ON storage.objects
FOR INSERT TO authenticated WITH CHECK (
bucket_id = 'profile-photos' AND
(storage.foldername(name))[1] = 'avatars' AND
auth.uid()::text = (storage.foldername(name))[2]
);
-- Photos are public (read)
CREATE POLICY "Photos public" ON storage.objects
FOR SELECT TO public USING (bucket_id = 'profile-photos');
```
The full guide also adds matching UPDATE and DELETE policies.
In `SupabaseProfilePhotoStorageClient.swift`:
* Uncomment `import Supabase`
* Remove comment blocks
```swift theme={null}
self.profilePhotoStorageClient = SupabaseProfilePhotoStorageClient(
supabaseClient: supabaseClient,
bucketName: "profile-photos"
)
```
## How It Works
```text theme={null}
Upload Flow:
User selects photo → Compress (~500KB) → Upload to Supabase → Update profile
Download Flow:
App opens → Load from Supabase → Cache locally → Display
```
**Benefits:**
* ✅ Photos sync across devices
* ✅ Automatic compression
* ✅ Public CDN URLs
* ✅ Graceful fallback to local
* ✅ Privacy-first (iOS 17+ PhotosPicker)
## Prerequisites
* Supabase project configured
* Supabase dependency in Storage package
* User authenticated
## What You Get
After setup:
* ✅ Upload profile photos
* ✅ Photos sync across devices
* ✅ Automatic compression
* ✅ CDN-hosted images
* ✅ Local fallback if upload fails
* ✅ No camera permissions needed (PhotosPicker)
## Complete Guide
The **12KB comprehensive guide** includes:
* Storage bucket configuration
* Row Level Security policies
* Upload implementation
* Compression strategy
* Error handling
* Testing procedures
Find complete profile photo storage guide in your project
## Related Guides
Storage architecture
Another optional sync feature
Required backend
Customize features
## Need Help?
* 📖 Complete troubleshooting in full guide
* 💬 [Create an issue](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/issues)
* 🔍 Check Supabase Storage logs
# RevenueCat Setup
Source: https://docs.swiftaiboilerplate.com/pages/guides/revenuecat-setup
Complete guide to configuring in-app subscriptions with RevenueCat
**Find complete step-by-step guide in your project:** [revenuecat.md](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/blob/main/docs/integrations/RevenueCat.md)
## What You'll Set Up
Monthly and annual subscription plans
Beautiful subscription UI (already built!)
## Prerequisites
* Apple Developer account (\$99/year)
* [RevenueCat account](https://revenuecat.com) (free tier available)
* Completed [Supabase Setup](/pages/guides/supabase-setup)
Subscriptions are the trickiest integration to get right. Work through each step in order, and reach out if you get stuck.
## Part 1: App Store Connect Setup
Go to **App Store Connect** → **Certificates, Identifiers & Profiles** → **Identifiers** (left menu) → **Add new identifier**.
Create a new App ID with your bundle identifier (e.g., `com.yourcompany.yourapp`).
Select your App ID and enable the required capabilities:
* ✅ Push Notifications
* ✅ In-App Purchase
* ✅ Sign in with Apple
* ✅ Associated Domains
* ✅ Any other capabilities you need
Go to **Apps** in App Store Connect and click **Create new app** (+ button).
Fill the form with your bundle ID and create the app:
* Platform: iOS
* Name: Your App Name
* Primary Language: English (or your language)
* Bundle ID: Select the one you created
* SKU: A unique identifier (e.g., `yourapp2024`)
If you're adding subscriptions:
1. Go to **Subscriptions** from the left menu
2. Click **Create** to add a new subscription group
3. Name it (e.g., "Premium Access")
Create subscriptions under the subscription group:
* Add **Monthly** subscription (e.g., `monthly_subscription`)
* Add **Annual** subscription (e.g., `annual_subscription`)
Make sure to note down the **Product IDs** exactly as you create them. Arrange their order in **descending order** as described by Apple (longest duration first).
## Part 2: RevenueCat Dashboard Setup
Go to [RevenueCat](https://app.revenuecat.com) and create a new project:
1. Click **Create New Project**
2. Name your project
3. Select **iOS** as platform
Create a new offering in RevenueCat:
1. Go to **Products** → **Entitlements**
2. Create entitlement named `pro` (or your choice)
3. Go to **Offerings** and create a new offering
4. Add your subscription products to the offering
Make **100% sure** the entitlement name matches what you use in your app code and note it down.
The RevenueCat SDK is already installed in the boilerplate!
Just make sure:
* Package dependencies are resolved in Xcode
* You have your **test API key** ready
The remaining steps RevenueCat suggests (SDK initialization, configuration, entitlement checks) are already wired up in the `Payments` package, so you only need the API key and entitlement ID.
Add your test API key to `Config/Secrets.xcconfig`:
```text theme={null}
REVENUECAT_API_KEY = appl_YOUR_TEST_API_KEY
RC_ENTITLEMENT_ID = pro
```
Then run the update config script again:
```bash theme={null}
bash scripts/update-config.sh
```
This regenerates `SwiftAIBoilerplatePro/Generated/Configuration.swift` with your values.
## Part 3: Testing Configuration
In Xcode, configure the scheme for testing:
1. Select your target from top menu
2. **Edit Scheme** → **Arguments** tab
3. Under **Environment Variables**, set `AUTH_BYPASS` to `0` (or remove it)
For **simulator testing**:
1. Select your target → **Edit Scheme**
2. Go to **Options** tab
3. **StoreKit Configuration** → select the bundled `StoreKitConfiguration.storekit` (in `SwiftAIBoilerplatePro/Resources/`)
For **real device testing**, leave StoreKit Configuration set to **None**.
Make sure the statuses of your subscriptions are **Ready to Submit** in App Store Connect.
Check each subscription product and ensure all required fields are filled.
Configure your App Store Connect API Key in RevenueCat:
Follow RevenueCat's official documentation for App Store Connect API key setup
We're not documenting this step in detail because when Apple changes it, RevenueCat updates their docs immediately.
In your RevenueCat offering, make sure to map the correct test product with your real App Store product.
Product IDs must match **exactly**. For example, if your 12-month subscription is mapped to a 1-month test product, it won't work. They must be mapped correctly.
Run your app and navigate to the paywall. You should see your subscription offerings loading correctly.
Detailed live RevenueCat logs are added to the project. If you have problems fetching products, analyze the console logs.
## Part 4: Sandbox Testing
Create a sandbox test account:
1. Go to **App Store Connect**
2. **Users and Access** → **Sandbox**
3. Create a new sandbox tester account
On your simulator or device:
1. Go to **Settings** → **Developer**
2. Sign in with your **Sandbox Account**
3. Try purchasing from your app
Be aware of this common issue: **Bundle ID mismatch** between App Store Connect and RevenueCat.
Make sure to upload a build to App Store Connect first. This registers your bundle ID properly and resolves many "products not found" issues.
Check the RevenueCat customers dashboard after making a test purchase.
It may take some time (up to an hour) for transactions to appear in the dashboard.
## What You Get
After setup:
* ✅ Working subscription purchases
* ✅ Beautiful paywall UI
* ✅ Subscription status tracking
* ✅ Restore purchases functionality
* ✅ Simulator testing with StoreKit Configuration
* ✅ Detailed logging for debugging
## Product Setup Example
**Monthly Subscription:**
* Product ID: `monthly_subscription`
* Type: Auto-Renewable Subscription
* Price: \$9.99/month
**Annual Subscription:**
* Product ID: `annual_subscription`
* Type: Auto-Renewable Subscription
* Price: \$79.99/year
## Troubleshooting
* Verify Product IDs match exactly in App Store Connect, RevenueCat, and StoreKit config
* Upload a build to App Store Connect first
* Check RevenueCat logs in console
* Ensure subscription status is "Ready to Submit"
* Sign out of real App Store account
* Sign in with sandbox account in Settings → Developer
* Restart the app
* Try again
* Verify entitlement name matches in code and RevenueCat dashboard
* Check that products are correctly mapped to entitlement
* Wait a few minutes and check RevenueCat dashboard
* Ensure bundle ID in Xcode matches App Store Connect
* Upload a test build to App Store Connect
* Re-sync products in RevenueCat
## Related Guides
Technical documentation
Paywall UI details
Customize subscriptions
Configure backend first
## Need Help?
* 📖 Check the troubleshooting section above
* 💬 [Create an issue](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/issues)
* 🔍 Search [RevenueCat docs](https://docs.revenuecat.com)
# Agentic Development Setup
Source: https://docs.swiftaiboilerplate.com/pages/guides/skills-setup
Set up Claude Code for agentic iOS development with SwiftAI Boilerplate Pro
SwiftAI Boilerplate Pro ships with a production-tested `AGENTS.md` at the repository root that gives Claude Code and Claude-powered agents project context from the first message. It points the agent to the full guidelines in `docs/CLAUDE.md`. Combined with community skills, Claude understands your architecture, follows your patterns, and can build and verify features autonomously.
**Ready out of the box.** `AGENTS.md` is at the root of the boilerplate and references `docs/CLAUDE.md`. Open Claude Code or Xcode Coding Intelligence from the project directory so the agent can load these automatically.
## How It Works
`AGENTS.md` and `docs/CLAUDE.md` tell Claude about **your project**: the MVVM architecture, module boundaries, which abstractions to use, build commands, and what patterns to follow.
Community skills teach Claude about **iOS development in general**: SwiftUI best practices, App Store guidelines, simulator automation.
| Layer | What It Provides | File |
| -------------------- | ------------------------------------------------- | ------------------------------------- |
| **Project-specific** | Architecture, abstractions, module boundaries | `AGENTS.md` (root) → `docs/CLAUDE.md` |
| **Domain-wide** | iOS best practices, debugging, testing, App Store | Community skills |
Together, they let Claude generate code that fits your project and follows platform conventions.
***
## Xcode Coding Intelligence
Xcode Coding Intelligence with Claude Agent works well with SwiftAI Boilerplate Pro as long as the agent is opened from the repository root. The important files are:
```text theme={null}
SwiftAIBoilerplatePro/
├── AGENTS.md
├── SwiftAIBoilerplatePro.xcodeproj
├── Packages/
└── docs/
└── CLAUDE.md
```
`AGENTS.md` must stay in the project root; it points the agent to `docs/CLAUDE.md`. If an agent says it cannot see the project instructions, check that you opened the repository folder itself, not only the `docs/` folder or an individual Xcode file.
Open the root folder that contains `AGENTS.md` and `SwiftAIBoilerplatePro.xcodeproj`.
Use Xcode Coding Intelligence from that project context. The boilerplate instructions are already written for SwiftUI, MVVM, Swift Packages, Supabase, RevenueCat, OpenRouter, and strict concurrency.
Ask: "Before coding, summarize the rules from `docs/CLAUDE.md` that apply to this project." If the answer does not mention SwiftAI Boilerplate Pro modules and build commands, restart the agent from the repository root.
The auto-loaded entry point is the root `AGENTS.md`, which references `docs/CLAUDE.md`. The other files under `docs/` are human-readable guides and deeper references.
***
## Recommended Skills
You can install community skills to extend Claude's iOS knowledge. Installation methods differ by skill:
* Commands that start with `/plugin` run inside **Claude Code or Claude Agent chat**.
* Commands that start with `git`, `cp`, or `npx` run in your **terminal**.
Apple platform conventions, Xcode project structure, SwiftUI, App Store review awareness, and platform-idiomatic patterns.
**Run in terminal:**
```bash theme={null}
git clone https://github.com/rshankras/claude-code-apple-skills.git
mkdir -p .claude/skills
cp -R claude-code-apple-skills/skills/* .claude/skills/
```
**Source:** [rshankras/claude-code-apple-skills](https://github.com/rshankras/claude-code-apple-skills)
Lets Claude boot, build, install, and interact with the iOS Simulator directly. Your AI can run and test the app it builds.
**Run in Claude chat:**
```text theme={null}
/plugin marketplace add conorluddy/ios-simulator-skill
/plugin install ios-simulator-skill@conorluddy
```
**Source:** [conorluddy/ios-simulator-skill](https://github.com/conorluddy/ios-simulator-skill)
State management (`@State`, `@Binding`, `@Observable`), view architecture, performance, accessibility, animations, and Liquid Glass (iOS 26).
**Run in terminal:**
```bash theme={null}
npx skills add AvdLee/SwiftUI-Agent-Skill --all
```
**Source:** [AvdLee/SwiftUI-Agent-Skill](https://github.com/AvdLee/SwiftUI-Agent-Skill)
The boilerplate works without community skills. They are optional additions on top of the included `AGENTS.md` and `docs/CLAUDE.md`, and community repositories may change their install commands over time.
**App Generator is a separate product (coming soon).** The agentic workflow on this page uses *your own* coding-agent CLI on the boilerplate source. The App Generator is a standalone macOS tool that drives your agent CLI to scaffold a differentiated app from the template; it ships separately and is not bundled with the boilerplate. See [License & Updates](/pages/reference/license) for editions and availability.
***
## Skill-Module Mapping
Which skills help most when working on each boilerplate module:
| Module | Best Skills | Why |
| ------------------------- | --------------------------------------------- | ------------------------------------------------------ |
| **FeatureChat** | SwiftUI Best Practices, iOS Simulator | View architecture, state management, simulator testing |
| **Auth** | Apple Platform Skills | App Store guidelines, security |
| **Payments** | Apple Platform Skills | In-app purchase compliance |
| **AI** | SwiftUI Best Practices | Async/await, streaming patterns |
| **Storage** | SwiftUI Best Practices | SwiftData best practices |
| **DesignSystem** | SwiftUI Best Practices, Apple Platform Skills | Accessibility, theming |
| **Rating prompt feature** | Apple Platform Skills | SKStoreReviewController guidelines |
| **Localization** | Apple Platform Skills, SwiftUI Best Practices | L10n, VoiceOver |
***
## Further Reading
* **[AI-Assisted Development](/pages/guides/ai-assisted-development)**: Cursor and Bolt AI configurations
# Supabase Setup
Source: https://docs.swiftaiboilerplate.com/pages/guides/supabase-setup
Complete guide to setting up Supabase for authentication, database, and AI proxy.
**Find complete step-by-step guide in your project:** [Supabase.md](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/blob/main/docs/integrations/Supabase.md)
## What You'll Set Up
Email/password and Apple Sign In authentication
Secure Edge Function for OpenRouter API calls
## Prerequisites
* [Supabase account](https://supabase.com) (free tier available)
* [OpenRouter account](https://openrouter.ai) (pay-as-you-go)
* Supabase CLI installed:
```bash theme={null}
brew install supabase/tap/supabase
# or: npm install -g supabase
```
## Step-by-Step Setup
Go to Supabase dashboard and navigate to **Project Settings**.
This is where you'll find all the credentials needed to connect your Swift app.
Find the **API Keys** tab on the left menu, then copy your **anon-public key**.
This is a public key that's safe to include in your iOS app bundle.
Write down your project ID. The easiest way is usually copying it from the address bar of your browser.
```text theme={null}
https://supabase.com/dashboard/project/YOUR_PROJECT_ID
```
Open `Config/Secrets.xcconfig` in Xcode and add your credentials:
```text theme={null}
SUPABASE_URL = https://YOUR_PROJECT_ID.supabase.co
SUPABASE_ANON_KEY = your-anon-public-key
```
The Secrets.xcconfig file must be under the **Config** folder in your project root.
Open Terminal, navigate to your repo, and login to Supabase:
```bash theme={null}
cd ~/path-to-your-repo
supabase login
```
Follow the instructions (press Enter to open browser and login).
Link your project to your repo:
```bash theme={null}
supabase link --project-ref YOUR_PROJECT_ID
```
Create the generated directory for your configuration file:
```bash theme={null}
mkdir -p "SwiftAIBoilerplatePro/Generated"
```
If you renamed the app target folder, replace `SwiftAIBoilerplatePro` with your actual app folder name.
Run the update config script to generate your Swift configuration file:
```bash theme={null}
bash scripts/update-config.sh
```
This generates a new Swift file including your public keys. It is safe to have this in the bundle.
For ease of development, temporarily disable the **Confirm email** option from Supabase:
1. Go to **Authentication** → **Providers** → **Email**
2. Disable **Confirm email**
Remember to re-enable this before production launch if you want email verification.
## What You Get
After setup:
* ✅ Real email/password authentication
* ✅ Sign in with Apple working
* ✅ Secure session management with auto-refresh
* ✅ Production-ready backend
## Continue Setup
After completing Supabase setup, continue with:
Deploy AI Edge Function
Configure subscriptions
## Related Guides
Optional: Sync conversations across devices
Optional: Cloud photo storage
Complete customization guide
Technical documentation
## Troubleshooting
* Verify SUPABASE\_URL and SUPABASE\_ANON\_KEY are correct
* Run `bash scripts/update-config.sh` again
* Clean and rebuild in Xcode (⌘⇧K then ⌘B)
* Make sure you created the Generated directory
* Check the path matches your project structure
* Run the update-config script from the project root
* Make sure you have Supabase CLI installed
* Try `supabase logout` then `supabase login` again
* Check your internet connection
## Need Help?
* 📖 Check the troubleshooting section above
* 💬 [Create an issue](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/issues)
* 🔍 Search [Supabase docs](https://supabase.com/docs)
# Testing Guide
Source: https://docs.swiftaiboilerplate.com/pages/guides/testing
Run the full ~598-test suite in one pass, write tests, and measure coverage
The v2.2.0 boilerplate ships with roughly 598 tests that all run in a single pass through `Boilerplate.xctestplan`. One `xcodebuild test` against the `SwiftAIBoilerplatePro` scheme exercises every package test target plus the app's unit and UI suites.
**Complete testing strategy in your project:** [TestingStrategy.md](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/blob/main/docs/foundations/TestingStrategy.md)
## Test Suite Overview
`Boilerplate.xctestplan` wires up 14 test targets: the 12 package test targets and the app's two targets.
`CoreTests`, `TestSupportTests`, `NetworkingTests`, `StorageTests`, `AuthTests`, `DesignSystemTests`, `FeatureChatTests`, `FeatureRatingTests`, `FeatureSettingsTests`, `PaymentsTests`, `LocalizationTests`, `AITests`
`SwiftAIBoilerplateProTests` (unit + integration) and `SwiftAIBoilerplateProUITests` (end-to-end XCUITest flows)
`TestSupport` is the v2.2 test-infrastructure package (it exposes `URLProtocolStub` for network stubbing). It is not one of the 11 reusable product packages — it exists only to support the test targets.
**Total: \~598 tests, one test-plan run.**
## Run Tests
```text theme={null}
# Run the whole test plan
⌘ + U
# Run a single test — click the diamond next to the test function
# Run a test class — click the diamond next to the class name
```
```bash theme={null}
# Whole workspace via the test plan
xcodebuild test \
-scheme SwiftAIBoilerplatePro \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.2'
# Convenience wrapper with a coverage report
./scripts/run-tests.sh --coverage --open
# A single package (runs `swift test` inside Packages/)
./scripts/run-tests.sh --package Core --coverage
```
The repo targets the **iPhone 17 Pro, OS=26.2** simulator (the destination CI uses). `scripts/run-tests.sh` itself drives the app test target on **iPhone 16, OS=18.6** to exercise the pre-iOS-26 `SAIGlass` Material fallback — pass any installed destination by editing the script's `DESTINATION` if your simulators differ.
## Coverage
Coverage is measured with `xccov`, not asserted per module. `scripts/run-tests.sh --coverage` writes `coverage-report.txt` and an HTML report, and warns (does not fail) if overall app coverage drops below its local `COVERAGE_THRESHOLD` of 85%.
CI enforces a separate **product-coverage floor of 25%** (`MINIMUM_COVERAGE` in `.github/workflows/ci.yml`). It computes coverage over product targets only — test targets and vendored dependencies (`SnapshotTesting`, `InlineSnapshotTesting`, `OneSignal`) are excluded so they don't deflate the number.
## Writing Tests
The packages use XCTest with protocol-based fakes. The patterns below mirror the suites that ship in the repo.
### Unit Test Example
`ChatViewModel` is constructed with `conversationID`, a `MessageRepository`, and an `LLMClient`. You set `inputText` and call `send()`. Tests inject `FakeMessageRepository` and `FakeLLMClient` (from `FeatureChatTests`).
```swift theme={null}
@MainActor
final class ChatViewModelTests: XCTestCase {
func testSendAppendsUserMessage() async {
// Arrange
let fakeRepo = FakeMessageRepository()
let fakeLLM = FakeLLMClient(chunks: ["Hello", " ", "world"])
let viewModel = ChatViewModel(
conversationID: UUID(),
messageRepository: fakeRepo,
llmClient: fakeLLM
)
// Act
viewModel.inputText = "Hello, AI!"
await viewModel.send()
// Assert — the user message landed and the input cleared
let userMessages = viewModel.messages.filter { $0.role == .user }
XCTAssertEqual(userMessages.count, 1)
XCTAssertEqual(viewModel.inputText, "")
}
}
```
### Integration Test Example
`CompositionRootTests` wires the real `CompositionRoot` with test configs and asserts the dependency graph is built.
```swift theme={null}
@MainActor
final class CompositionRootTests: XCTestCase {
func testGraphIsWired() throws {
let authConfig = AuthConfig(
supabaseURL: try XCTUnwrap(URL(string: "https://test.supabase.co")),
supabaseAnonKey: "test_anon_key"
)
let paymentsConfig = PaymentsConfig(
apiKey: "test_api_key",
entitlementID: "test_entitlement"
)
let composition = try CompositionRoot(
authConfig: authConfig,
paymentsConfig: paymentsConfig
)
XCTAssertNotNil(composition.httpClient)
XCTAssertNotNil(composition.sessionManager)
XCTAssertNotNil(composition.makeChatViewModel(conversationID: UUID()))
}
}
```
### UI Test Example
UI tests set `UI_TESTING=1` in the launch environment so the app skips the first-launch onboarding/permission prompts that would otherwise block automation. They drive the app by its real accessibility labels.
```swift theme={null}
func testChatFlow() throws {
let app = XCUIApplication()
app.launchEnvironment["UI_TESTING"] = "1"
app.launch()
app.buttons["Debug: Mock Sign In"].tap()
app.tabBars.buttons["Chat"].tap()
app.buttons["New Chat"].firstMatch.tap()
let input = app.textFields["Type a message..."]
input.tap()
input.typeText("Hello, test message")
app.buttons["Send"].tap()
XCTAssertTrue(app.staticTexts["Hello, test message"].waitForExistence(timeout: 5))
}
```
## CI/CD Integration
`.github/workflows/ci.yml` runs on every push to `main`/`develop` and on pull requests targeting those branches. Its jobs:
* **Build & Test (iOS 26.2)** — builds and runs the full test plan on iPhone 17 Pro / iOS 26.2 with coverage, then enforces the 25% product-coverage floor.
* **Build & Test (iOS 18.6 fallback path)** — `test-ios18-fallback` rebuilds and runs the suite on iPhone 16 Pro / iOS 18.6 to prove the `SAIGlass` Material fallback still compiles and passes on pre-iOS-26 simulators. No coverage gate — it catches fallback-path regressions only.
* **Template Manifest** — validates `template.manifest.json` via `scripts/validate-template-manifest.sh`.
* **Secret Scan** — Gitleaks over the full history.
* **SwiftLint** — `swiftlint lint --strict`.
### Coverage Reports
* The iOS 26.2 job posts a coverage summary as a PR comment.
* `.github/workflows/coverage-report.yml` runs weekly (Mondays 09:00 UTC) and on demand, producing a detailed report and opening a GitHub issue if any module falls below the 25% threshold.
* `scripts/run-tests.sh --coverage --open` generates a local HTML breakdown.
## Test Organization
```text theme={null}
Boilerplate.xctestplan
├── App targets
│ ├── SwiftAIBoilerplateProTests # unit + integration (CompositionRoot, proxy, paywall)
│ └── SwiftAIBoilerplateProUITests # XCUITest end-to-end flows (auth, chat, settings)
│
└── Package test targets (12)
├── CoreTests
├── TestSupportTests
├── NetworkingTests
├── StorageTests
├── AuthTests
├── DesignSystemTests
├── FeatureChatTests
├── FeatureRatingTests
├── FeatureSettingsTests
├── PaymentsTests
├── LocalizationTests
└── AITests
```
## Best Practices
**Do test:**
* ✅ ViewModels (business logic)
* ✅ Repositories (data access)
* ✅ Clients (external services)
* ✅ Error scenarios
* ✅ Edge cases
**Don't test:**
* ❌ SwiftUI Views (snapshot instead)
* ❌ Third-party libraries
* ❌ Framework code
* Use Arrange-Act-Assert pattern
* One assertion per test (when possible)
* Clear test names (testSendMessage\_WhenOffline\_ShowsError)
* Mock external dependencies
* Test in isolation
* Protocol-based mocking
* Track call counts
* Configurable behavior
* Avoid over-mocking
* Use real objects when simple
## Coverage Measurement
```bash theme={null}
# Generate the local HTML report and open it
./scripts/run-tests.sh --coverage --open
# Coverage for one package only
./scripts/run-tests.sh --package Core --coverage
```
The local report warns below 85% app coverage; CI gates only the 25% product-coverage floor described under [CI/CD Integration](#cicd-integration).
## View Complete Guide
Complete guide with examples, patterns, and best practices
## Troubleshooting
* Match the CI destination: iPhone 17 Pro, OS=26.2
* Confirm Xcode 26.2+ (CI selects Xcode 26.2)
* Clear derived data, then rebuild
* Look for timing-sensitive async tests
* Run with `--coverage`
* Add tests for untested branches and error scenarios
* Make sure `async` code is actually awaited in the test
* Set `app.launchEnvironment["UI_TESTING"] = "1"` so the app skips the first-launch onboarding/permission alert that blocks automation
* Use `waitForExistence(timeout:)` instead of bare `exists`
* Drive elements by their accessibility labels (e.g. `"Send"`, `"Type a message..."`)
* Avoid asserting mid-animation
# Welcome to SwiftAI Boilerplate Pro
Source: https://docs.swiftaiboilerplate.com/pages/index
Swift 6 + iOS 26 Liquid Glass iOS boilerplate for AI-powered apps. Streaming chat, Supabase auth, RevenueCat subscriptions, and 11 clean Swift Packages.
**Ship your AI-powered iOS app in days, not months.**
SwiftAI Boilerplate Pro is a production-ready iOS foundation built by an iOS engineer with 10 years of commercial experience, who has shipped multiple production apps. It includes everything you need to build and launch AI chat applications with authentication, subscriptions, and analytics out of the box.
**v2.2.0 is live: the App Store Guideline 4.3(a) playbook** — a Release-binary audit gate plus hardening checklist and prompt packs that fail your build until the app is rebranded and 4.3-safe. Requires Xcode 26.2+. See the [changelog](/pages/reference/changelog) and the [migration guide](/pages/migration/v1-9-to-v2-0).
All setup, integration, and publishing steps are explained here on these docs. For customization and design patterns, see the corresponding files under /docs in your project.
**App Generator is a separate product (coming soon).** A standalone macOS app that drives your own coding-agent CLI to generate a differentiated, 4.3-safe app from this template — it ships no LLM and is sold separately. Existing and past buyers are grandfathered: lifetime access to all new boilerplate versions (v2.2.0 and beyond, no new charge), plus App Generator access when it ships. See the [license & updates model](/pages/reference/license) for the full breakdown.
**Found an issue or confusing setup step?** Please open a [GitHub issue](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/issues). Include your Xcode version, Swift version, boilerplate tag or branch, the exact error text, and the steps you took so we can reproduce it quickly.
## What You Get
Release-binary audit gate, hardening checklist, and prompt packs that keep your app 4.3-safe
Claude Code, Cursor AI, and Bolt AI configurations included
Streaming responses via OpenRouter (500+ models)
Supabase Auth + Sign in with Apple, ready to go
RevenueCat integration with paywall UI
5 themes, tokens, and components
\~598 tests across 12 package test targets + the app test suites, in one test-plan run
## Quick Stats
* **275+ hours** of development already done
* **\~598 tests** across 12 package test targets + the app test suites, in one test-plan run
* **11 Swift Packages** cleanly separated for reuse in any iOS project
* **AI-Assisted Development** with Claude Code CLAUDE.md, Cursor AI rules, and Bolt AI chat modes
* **Comprehensive docs** with examples and LLM prompt packs
* **2 chat UIs** (bubble and centered styles)
## Time Saved
This boilerplate typically saves **5-8 weeks** of build time and $20,000-$30,000 in development cost compared to building from scratch.
Module savings are conservative estimates based on prior builds; your mileage may vary by scope and team experience.
See the complete feature breakdown in [Features Reference](/pages/reference/features).
## Get Started in 3 Steps
Clone the repo and run the app with mock backends
[View Installation Guide →](/pages/installation)
Go from zero to a running app in 10 minutes
[View Quick Start →](/pages/quickstart)
Follow the in-doc customization roadmap, then use the deeper files in your project’s `/docs` folder when you need more detail.
[View Building Your App →](/pages/guides/building-your-app)
## Production Setup Order
After the mock app runs, use this sequence to connect real services:
| Step | Goal | Guide |
| ---- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Create Supabase project and secrets | [Supabase Setup](/pages/guides/supabase-setup) |
| 2 | Configure Apple, Google, and email auth | [Authentication Guide](/pages/guides/authentication) |
| 3 | Deploy the OpenRouter AI proxy | [Deployment Guide](/pages/guides/deployment#openrouter-setup) |
| 4 | Set up subscriptions and entitlements | [RevenueCat Setup](/pages/guides/revenuecat-setup) |
| 5 | Add optional services | [Crashlytics](/pages/guides/crashlytics-setup), [OneSignal](/pages/guides/onesignal-setup), [Chat Sync](/pages/guides/chat-sync), [Profile Photos](/pages/guides/profile-photos) |
| 6 | Prepare App Store submission | [App Store 4.3 Hardening](/pages/guides/app-store-4-3-hardening) |
## Architecture Overview
Built with modern iOS development best practices:
* **Swift 6 strict concurrency** - `@MainActor`, `@Observable`, explicit `any`, Sendable boundaries
* **iOS 26 Liquid Glass** - `SAIGlass` primitive with automatic Material fallback on iOS 17–25
* **MVVM Architecture** - Clean separation of concerns
* **Dependency Injection** - CompositionRoot pattern
* **Protocol-Oriented** - Swappable implementations
* **SwiftData** - Modern persistence (iOS 17+)
[Learn about the architecture →](/pages/architecture)
## Foundation Modules
Error handling, logging, utilities
**Saves: 6–10 hrs**
HTTP client with interceptors
**Saves: 12-20 hrs**
SwiftData + Keychain + Cloud sync
**Saves: 16-24 hrs**
Supabase + Apple + Google + Email
**Saves: 20-32 hrs**
RevenueCat subscriptions
**Saves: 16-24 hrs**
LLM client with streaming
**Saves: 24-40 hrs**
Complete chat UI (2 styles)
**Saves: 40-60 hrs**
Settings + Paywall UI
**Saves: 12-16 hrs**
Design tokens, 5 themes, and components
**Saves: 20-32 hrs**
## Next Steps
Get the boilerplate running locally
10-minute guide to first run
Understand the system design
Complete customization guide
## Need Help?
* [Complete Documentation Hub](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/blob/main/docs/INDEX.md)
* [GitHub Issues](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/issues)
* Use the built-in [CLAUDE.md](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/blob/main/docs/CLAUDE.md) guide for AI-assisted development
# Installation
Source: https://docs.swiftaiboilerplate.com/pages/installation
Get the boilerplate running in **under 10 minutes**.
## Prerequisites
Before starting, ensure you have:
* **macOS 15+ with Xcode 26.2+**. v2.2.0 uses the iOS 26 SDK to compile Liquid Glass APIs (`Glass`, `glassEffect`, `GlassEffectContainer`). Runtime still supports iOS 17+ via a SwiftUI `Material` fallback built into `SAIGlass`.
* Swift 6.0 toolchain (bundled with Xcode 26.2+)
* Git installed
No backend setup required for initial run. The app includes mock backends for immediate development.
**Using older Xcode?** Stay on the [`v1.9.0`](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/releases/tag/v1.9.0) tag until you can upgrade. v2.0 and later use iOS 26 SDK symbols that older toolchains cannot compile, regardless of `#available` checks.
## Step 1: Get the Repository
You have three options to access the boilerplate:
Clone the repository to get started immediately with easy updates:
```bash theme={null}
git clone https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution.git
cd SwiftAIBoilerplatePro-Distribution
```
Pull updates anytime with `git pull`.
Fork the repository to your own GitHub account for full control:
1. Click **Fork** on the repository page
2. Clone your fork:
```bash theme={null}
git clone https://github.com/YOUR_USERNAME/SwiftAIBoilerplatePro-Distribution.git
cd SwiftAIBoilerplatePro-Distribution
```
Sync upstream updates when needed.
Download directly without Git:
1. Go to the repository page
2. Click **Code** → **Download ZIP**
3. Extract and open in Xcode
**Keep it private!** All forks and copies must remain in **private repositories**. Do not make your fork public or share the boilerplate code publicly. This protects the code and your [license](/pages/reference/license).
## Step 1.1: Version Control Safely
If you cloned the boilerplate, do not push your app changes back to the boilerplate repository. The safest workflow is to keep two Git remotes:
* `upstream`: the original SwiftAI Boilerplate Pro repository, used only to pull future boilerplate updates
* `origin`: your private repository, used for your app's code
After cloning, Git names the boilerplate repository `origin`. Rename it so normal pushes do not go back to the boilerplate source.
```bash theme={null}
git remote rename origin upstream
git remote -v
```
In GitHub, GitLab, or Bitbucket, create a new **private** repository for your app. Do not initialize it with a README, license, or `.gitignore` because the boilerplate already includes project files.
Replace the URL with your private repository URL:
```bash theme={null}
git remote add origin https://github.com/YOUR_USERNAME/YOUR_PRIVATE_REPO.git
```
Most projects use `main`. If your local branch is named `master`, replace `main` with `master`.
```bash theme={null}
git push -u origin main
```
### Daily Git Workflow
After setup, your normal commits go to your private repository:
```bash theme={null}
git add .
git commit -m "Add my custom feature"
git push
```
### Pull Boilerplate Updates Later
When a new boilerplate version ships, pull it from `upstream` and then push the merged result to your private `origin`:
```bash theme={null}
git fetch upstream
git merge upstream/main
git push
```
Future-version access depends on your tier. The **$199 lifetime license** includes all future versions and updates; the **$99 single download** is a current-version snapshot only. Existing buyers keep lifetime access (and get App Generator access when it ships). See [License & updates](/pages/reference/license).
### Handling Merge Conflicts
Merge conflicts can happen when you and the boilerplate both changed the same lines. Git will pause the merge and mark the files with conflict markers:
```text theme={null}
<<<<<<< HEAD
your app changes
=======
new boilerplate changes
>>>>>>> upstream/main
```
Open each conflicted file, keep the final code you want, delete the marker lines, then finish the merge:
```bash theme={null}
git status
git add .
git commit -m "Resolve upstream merge conflicts"
git push
```
Keep custom app code in your private `origin`. Use `upstream` only for reading and merging official boilerplate updates.
## Step 2: Open in Xcode
```bash theme={null}
open SwiftAIBoilerplatePro.xcodeproj
```
The project uses Swift Package Manager. Xcode will automatically resolve dependencies on first open (takes \~30 seconds).
If you use `xcodes` or `asdf-xcode`, the repo pins `26.3` via `.xcode-version`. Run `xcodes install` (or `asdf install xcode`) inside the repo to match.
## Step 3: Configure Environment (Optional)
**For immediate testing:** You can skip this step! The app runs with mock services out of the box.
**For production services:**
1. **Copy the config template:**
```bash theme={null}
cp Config/Secrets.example.xcconfig Config/Secrets.xcconfig
```
2. **Fill in your API keys** in `Config/Secrets.xcconfig`
3. **IMPORTANT - Update configuration:**
```bash theme={null}
bash scripts/update-config.sh
```
Every time you change `Config/Secrets.xcconfig`, you **must** run `bash scripts/update-config.sh` and rebuild. This is **not automatic**.
**For immediate testing**, you can leave the default placeholder values! The app will use:
* `MockAuthClient` (no Supabase needed)
* `EchoLLMClient` (echoes messages back)
* No payment functionality
## Step 4: Build and Run
Press **⌘ + R** in Xcode or:
```bash theme={null}
xcodebuild -scheme SwiftAIBoilerplatePro \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.2'
```
## What You'll See
On first run, the app will:
1. **Show onboarding** - 3 swipeable pages
2. **Display sign-in** - Use any email/password (mock auth)
3. **Home screen** - Feature cards and quick actions
4. **Chat works!** - Send messages, get echo responses
5. **Settings screen** - Explore different themes
If you see the home screen, you're all set! The app is running with mock backends.
## Project Structure
```text theme={null}
SwiftAIBoilerplatePro/
├── SwiftAIBoilerplatePro/ # Main app
│ ├── AppShell/ # UI screens
│ ├── Composition/ # DI container (LLMClientFactory, etc.)
│ ├── Generated/ # Configuration.swift (from update-config.sh)
│ └── Resources/ # Assets
├── Packages/ # 11 reusable Swift packages + TestSupport
│ ├── Core/ # Foundation
│ ├── Networking/ # HTTP client
│ ├── Storage/ # SwiftData
│ ├── Auth/ # Authentication
│ ├── Payments/ # Subscriptions
│ ├── AI/ # LLM client (re-exports FeatureChat)
│ ├── FeatureChat/ # Chat UI (owns LLMClient/LLMMessage)
│ ├── FeatureRating/ # In-app review prompts
│ ├── FeatureSettings/ # Settings
│ ├── Localization/ # Strings + AppError.localizedUserMessage
│ ├── DesignSystem/ # UI components (SAIGlass)
│ └── TestSupport/ # Test-only infra (URLProtocolStub), v2.2
├── Config/ # Build configuration
│ ├── App.xcconfig # APP_BUNDLE_ID, SWIFT_VERSION, deployment target
│ └── Secrets.xcconfig # Your API keys
├── Boilerplate.xctestplan # Single workspace test plan
└── supabase/ # Backend (optional)
└── functions/ai/ # AI proxy → OpenRouter
```
## Mock vs Production Mode
### Mock Mode (Default)
Perfect for:
* ✅ Rapid initial development
* ✅ UI testing without setup
* ✅ Exploring the codebase
* ✅ Avoiding API or subscription costs
**What's mocked:**
* Authentication (any email works)
* AI responses (echoed locally)
* Payments (simulated states)
### Production Mode
When you're ready to go live, switching to Production Mode is seamless.
This boilerplate is designed so you can move from mock data to real users and APIs in just a few setup steps, all clearly documented.
Required when you need:
* Real user authentication (via Supabase)
* Actual AI responses
* Working subscriptions and payments
To complete the backend setup see [Quick Start](/pages/quickstart) or [Setup Guides](/pages/guides/supabase-setup).
## Verify Installation
No build errors in Xcode
Run tests with **⌘ + U**. One pass of `Boilerplate.xctestplan` runs \~598 tests across 12 package test targets + the app test suites — all should pass.
Navigate through onboarding → sign in → home → chat
## Common Issues
### Dependencies Won't Resolve
**Solution:**
```bash theme={null}
# In Xcode
File → Packages → Reset Package Caches
File → Packages → Update to Latest Package Versions
```
### Missing Config File
**Error:** `Config/Secrets.xcconfig not found`
**Solution:**
```bash theme={null}
cp Config/Secrets.example.xcconfig Config/Secrets.xcconfig
```
### Build Errors
**Solution:**
```bash theme={null}
# Clean build
⌘ + Shift + K
# Or from terminal
xcodebuild clean
```
## Next Steps
Get to your first running app
Understand the system design
Complete customization guide
Connect real services
## Development Tips
**Using AI assistants?** Check out [CLAUDE.md](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/blob/main/docs/CLAUDE.md) for guidelines on AI-assisted development.
**Want to customize immediately?** Read the [Building Your App guide](/pages/guides/building-your-app) which includes ready-to-use LLM prompts for common tasks.
# Upgrading from v1.9 to v2.0
Source: https://docs.swiftaiboilerplate.com/pages/migration/v1-9-to-v2-0
The full migration path for Swift 6, iOS 26 Liquid Glass, and the v2.0 architecture refactor.
v2.0 is the Swift 6 and iOS 26 Liquid Glass release. It changes the toolchain floor, ships a new Liquid Glass primitive, and splits oversized files into ≤ 400-line extension siblings. Most downstream apps upgrade in under an hour.
**Canonical release notes:** [`RELEASE_NOTES.md`](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/blob/main/RELEASE_NOTES.md) in the boilerplate repo. This guide is the step-by-step version.
## 1. Toolchain
v2.0 uses iOS 26 SDK symbols (`Glass`, `glassEffect`, `GlassEffectContainer`) that older toolchains cannot compile, regardless of `#available` checks.
**Required:**
* macOS 15+
* Xcode 26.2+ (Swift 6.0)
* iOS 17+ runtime (unchanged)
**The repo pins `26.3` via `.xcode-version`.** If you use `xcodes` or `asdf-xcode`, run `xcodes install` (or `asdf install xcode`) inside the repo to match.
**Stuck on older Xcode?** Pin your build to [`v1.9.0`](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/releases/tag/v1.9.0) until you can upgrade. There is no forward-compatible fallback; the iOS 26 SDK is required to compile.
## 2. Apply the Supabase migration
v2.0 ships a new SQL migration that fixes an authorization (IDOR) gap in the `get_conversation_with_stats` function — it was `SECURITY DEFINER` with no `auth.uid()` ownership check, so any authenticated user could read any conversation by UUID.
```bash theme={null}
# From the repo root
supabase migration up
```
Or run the file directly:
```bash theme={null}
psql "$SUPABASE_DB_URL" \
-f supabase/migrations/20260408000000_fix_conversation_stats_auth.sql
```
## 3. Redeploy the `ai` edge function
The Edge Function was updated alongside the migration. Redeploy it so the new version picks up the fix:
```bash theme={null}
supabase functions deploy ai
```
## 4. Install the secrets pre-commit hook
v2.0 ships a pre-commit hook that blocks accidental secret commits (the hook lives in `.githooks/`). Enable it once per clone:
```bash theme={null}
git config core.hooksPath .githooks
```
This is a one-time, per-clone setting. It is not picked up automatically.
## 5. Reapply downstream customisations
A lot of oversized files were split into extension siblings to stay under the new ≤ 400-line rule. Public entry points are unchanged, but if you customised any of these files, your edits may now belong in a subview or sibling file.
| Touched in v1.9 | Where the content lives now |
| ----------------------- | --------------------------------------------------------------------------------- |
| `ProfileView.swift` | `AppShell/Profile/*.swift` (composition root + section files) |
| `SettingsView.swift` | `FeatureSettings/Views/Settings/*.swift` (≤ 125-line root + one file per section) |
| `EmailSignUpView.swift` | `AppShell/Auth/EmailSignUp*.swift` |
| `SessionManager.swift` | `+SignIn`, `+Refresh`, `+Persistence` extensions |
| `L10n.swift` | `L10n+.swift` per nested enum |
| `ChatViewModel.swift` | `ChatViewModel+Memory.swift` for history/memory |
Strategy: `git log` the file, find the function you edited, `grep` for it in the new tree, port the change.
## 6. Fighting-glass cleanup
This is the biggest source of visual regressions when upgrading. On iOS 26, SwiftUI handles sheet and container materials for you. If your downstream code piles manual backgrounds on top, you block the Material SwiftUI already provides and you fight Liquid Glass on iOS 26.
**Remove these patterns from your own views:**
* `.background(DSColors.background)` on SwiftUI containers (`NavigationStack`, `Form`, `List`, sheets, `TabView`).
* `DSColors.background.ignoresSafeArea()` stacked under sheets or full-screen covers.
* `.background(.black)` or `.background(Color.clear)` under `.presentationBackground` on sheets.
* `.scrollContentBackground(.hidden)` on forms that already use `.formStyle(.grouped)`. `.grouped` already does the right thing on iOS 26.
* Explicit `.safeAreaInset(edge: .bottom)` gutters under a `TabView`. The tab bar's own inset is already respected.
**Use instead:**
* `.saiGlass(.regular)` when you need glass on a custom view.
* `SAIGlassContainer { ... }` when multiple glass surfaces sit near each other.
* `.saiScrollEdgeGlass(_:)` for scroll-edge treatment under nav and input chrome.
* `.saiTabBarMinimize(.onScrollDown)` if you want iOS 26 tab bar minimisation (it is a no-op on iOS 17–25).
## 7. Paywall CTA theming
Paywall CTAs in v2.0 use the standard system styles:
```swift theme={null}
Button("Upgrade") { ... }
.buttonStyle(.borderedProminent)
Button("Maybe later") { ... }
.buttonStyle(.bordered)
```
If you were theming these buttons via `.background()`, **switch to `.tint()`**:
```swift theme={null}
// Before (v1.9)
Button("Upgrade") { ... }
.background(BrandConfig.accentColor)
// After (v2.0)
Button("Upgrade") { ... }
.buttonStyle(.borderedProminent)
.tint(BrandConfig.accentColor)
```
`.tint()` lets the standard button style render the glass-aware surface correctly on iOS 26, and still picks up the correct Material on iOS 17–25.
## Quick verification checklist
After the upgrade, run through this list before shipping:
* [ ] App builds under Xcode 26.2+ with zero Swift 6 warnings.
* [ ] Supabase migration ran cleanly (`supabase migration list` shows `20260408000000_fix_conversation_stats_auth`).
* [ ] The `ai` Edge Function returns a streaming response end-to-end.
* [ ] `git config core.hooksPath` returns `.githooks`.
* [ ] Sheets, paywall, settings, and chat all look right on **iOS 26** (Liquid Glass) and **iOS 17–18** (Material fallback).
* [ ] No remaining `.onReceive(...)` on `ToastCenter` or `DeepLinkBus` in downstream code; migrate to `.onChange(of:)`.
* [ ] No remaining `.background(DSColors.background.ignoresSafeArea())` under containers.
## Related
Full v2.0.0 release entry
`SAIGlass`, modifiers, container
Swift 6 concurrency model + ≤ 400-line rule
Common upgrade errors and fixes
# Accessibility Module
Source: https://docs.swiftaiboilerplate.com/pages/modules/accessibility
Make your iOS app inclusive by default with VoiceOver, Dynamic Type, Reduce Motion, and High Contrast support
Every app deserves to be usable by everyone. The Accessibility module makes building inclusive iOS apps effortless with pre-defined labels, SwiftUI modifiers, and comprehensive support for iOS accessibility features.
**Package location:** `Packages/DesignSystem/Sources/DesignSystem/Accessibility/`
## Overview
The Accessibility module is part of the DesignSystem package and provides ready-to-use accessibility features that work with VoiceOver, Dynamic Type, Reduce Motion, and High Contrast modes out of the box.
Typed labels for common UI patterns, ready to apply
Text scales beautifully with user preferences
Respects motion sensitivity settings
Colors adapt for better readability
## Quick Start
### VoiceOver Labels
One line for full accessibility:
```swift theme={null}
import DesignSystem
// Apply pre-defined accessibility
Button("Send") { }
.saiAccessible(A11y.Chat.sendButton)
// VoiceOver: "Send message, button. Double tap to send your message."
// Works for all common UI patterns
Image("avatar")
.saiAccessible(A11y.Profile.photo)
// VoiceOver: "Profile photo. Double tap to change your profile photo."
```
### Dynamic Type Support
Text scales beautifully with user preferences:
```swift theme={null}
Text("Respects user settings")
.saiScaledFont(size: 18, weight: .semibold, relativeTo: .body)
// Automatically scales with iOS text size settings
// Capped to prevent layout issues at extreme sizes
```
### Reduce Motion
Respects users who experience discomfort with animations:
```swift theme={null}
Button("Animate") { }
.scaleEffect(isPressed ? 0.95 : 1.0)
.saiReducedMotionAnimation(.spring())
// Animation plays normally OR instant state change
// Based on user's Reduce Motion setting
```
### High Contrast Mode
Colors adapt automatically for users who need more contrast:
```swift theme={null}
Text("Adapts to user needs")
.saiContrastAwareForeground(
normal: DSColors.textSecondary,
highContrast: DSColors.textPrimary
)
```
## File Structure
```text theme={null}
Packages/DesignSystem/Sources/DesignSystem/Accessibility/
├── A11y.swift # Pre-defined labels (A11yLabel + A11y namespaces)
├── A11yModifiers.swift # SwiftUI modifiers (sai* View extensions)
└── A11yAudit.swift # Debug-only audit overlay + checklist
```
## Pre-Defined Labels
The `A11y` enum groups typed `A11yLabel` values by feature. Use `A11y..