Do less. Ship more.
A production-ready iOS app template with offline-first architecture. Battle-tested patterns extracted from a real production app.
Koalas sleep 22 hours a day. They're not lazy — they're efficient. The best programmers think the same way. This template lets you ship in hours, not weeks.
- Offline-First Architecture: Works without Firebase - toggle backend features with a single flag
- Authentication: Apple Sign-In + Google Sign-In with Firebase Auth (optional)
- Subscriptions: RevenueCat integration with paywall support
- Analytics: Privacy-preserving analytics with TelemetryDeck
- Streaks: Backend-driven (Firebase) or local-only streak system
- Library/CMS: GitHub-based content system with markdown rendering
- Widgets: Home screen and lock screen widget templates
- UI Theme: Centralized color system with dark mode support
The template supports two modes controlled by AppConfiguration.useFirebase:
- No Firebase required - app works standalone
- Settings stored in UserDefaults
- Streaks tracked locally on device
- Device-based user ID for RevenueCat/TelemetryDeck
- Perfect for simple apps or rapid prototyping
- Full Firebase integration (Auth, Firestore, Crashlytics)
- Apple/Google Sign-In with anonymous user linking
- Settings sync across devices via Firestore
- Backend-driven streak calculation with reminders
- Requires Firebase project setup
swiftui-indie-stack/
├── ios/ # iOS app (Xcode project)
│ └── Sources/ # Main app target
│ ├── App/ # App entry point, configuration
│ ├── Auth/ # Authentication (conditional Firebase)
│ ├── User/ # Settings, Firestore manager
│ ├── Streak/ # Streak system (cloud or local)
│ ├── Paywall/ # RevenueCat integration
│ ├── Analytics/ # TelemetryDeck wrapper
│ ├── Library/ # GitHub-based CMS
│ ├── TabBar/ # Custom tab bar
│ ├── Onboarding/ # Onboarding flow
│ ├── UI/ # Theme, components, modifiers
│ └── Utilities/ # Logging, haptics
├── firebase-functions/ # Firebase backend (optional)
│ └── functions/ # Cloud Functions source
└── content/ # CMS content (can be separate repo)
├── articles/ # Markdown articles
└── index.json # Content index
- Clone the repository:
git clone https://github.com/cliffordh/swiftui-indie-stack.git
cd swiftui-indie-stack/ios- Open in Xcode and configure:
// AppConfiguration.swift
static let useFirebase = false // Already set
static let useRevenueCat = true // Set up RevenueCat
static let useTelemetryDeck = true // Set up TelemetryDeck- Add your API keys:
static let revenueCatAPIKey = "appl_YOUR_KEY"
static let telemetryDeckAppID = "YOUR_APP_ID"- Build and run on simulator!
Running on device? Update the bundle identifier first: Project → Target → Signing & Capabilities → change
com.example.myappto your own (e.g.,com.yourcompany.yourapp). Simulator works without changes.
-
Complete Local Mode setup above
-
Create Firebase project:
- Go to console.firebase.google.com
- Enable Authentication (Apple, Google providers)
- Enable Firestore Database
- Download
GoogleService-Info.plist
-
Add Firebase to Xcode:
- Add
GoogleService-Info.plistto the project - Add Firebase SDK via SPM
- Add
-
Enable Firebase:
// AppConfiguration.swift
static let useFirebase = true- Deploy Firebase Functions:
cd firebase-functions/functions
npm install
firebase deployAll app configuration is in AppConfiguration.swift:
enum AppConfiguration {
// Feature toggles
static let useFirebase = false // Enable Firebase backend
static let useRevenueCat = true // Enable subscriptions
static let useTelemetryDeck = true // Enable analytics
static let enableStreaks = true // Enable streak feature
static let enableLibrary = true // Enable CMS feature
// API Keys
static let revenueCatAPIKey = "YOUR_KEY"
static let telemetryDeckAppID = "YOUR_APP_ID"
// URLs
static let libraryIndexURL = "https://raw.githubusercontent.com/..."
static let termsOfServiceURL = "https://..."
static let privacyPolicyURL = "https://..."
}Local Mode: Device-based identity for RevenueCat/TelemetryDeck. No sign-in UI.
Cloud Mode: Full Firebase Auth with Apple/Google Sign-In, anonymous auth, and credential linking.
// Check if sign-in is available
if AuthManager.shared.canSignIn {
// Show sign-in UI
}
// Get user ID (works in both modes)
let userId = AuthManager.shared.userIdLocal Mode: Simple on-device streak tracking with UserDefaults.
Cloud Mode: Backend-driven streaks with Firebase Functions handling calculation, at-risk detection, and reminders.
// Record activity (works in both modes)
if AppConfiguration.useFirebase {
FirestoreManager.shared.logActivity(type: "lesson_completed")
} else {
StreakDataProvider.shared.recordLocalActivity()
}
// Display streak
StreakBadgeView() // Shows current streak with animationRevenueCat integration for subscriptions. Works identically in both modes.
// Show paywall
PaywallManager.shared.triggerPaywall()
// Check subscription
let isSubscribed = await PaywallManager.shared.checkSubscriptionStatus()GitHub-based CMS for documentation. Independent of Firebase.
// Content is fetched from your GitHub repo
// Edit AppConfiguration.libraryIndexURL to point to your contentOffline-first settings with optional cloud sync.
Local Mode: All settings stored in UserDefaults via @AppStorage.
Cloud Mode: Settings sync to Firestore when changed.
// Settings always work locally
SettingsViewModel.shared.appearance = .dark
// If Firebase enabled, changes sync automaticallyThe firebase-functions/ directory contains the backend for cloud mode:
createUserRecord- Creates user document on signupupdateStreak- Calculates streak when activity loggedcheckStreaksAtRisk- Daily check (6 PM) for at-risk streaksresetBrokenStreaks- Daily reset (midnight) of broken streaks
Deploy with:
cd firebase-functions/functions
npm install
npm run deploy| Package | Version | Purpose | Required |
|---|---|---|---|
| RevenueCat | ~> 5.31 | Subscriptions | Optional |
| TelemetryDeck | ~> 2.9 | Analytics | Optional |
| Firebase | ~> 11.8 | Auth, Firestore | Optional |
| GoogleSignIn | ~> 8.0 | Google auth | Optional |
| swift-markdown-ui | ~> 2.4 | Markdown rendering | Yes |
| ConfettiSwiftUI | ~> 1.1 | Celebration effects | Yes |
| SwiftUI-Shimmer | ~> 1.5 | Loading effects | Yes |
| NetworkImage | ~> 6.0 | Async image loading | Yes |
- iOS 17.0+
- Xcode 15.0+
- Swift 5.9+
See CUSTOMIZATION.md for detailed guides on:
- Branding and colors
- Adding/removing tabs
- Customizing streaks
- Setting up the CMS
- Paywall configuration
MIT License - see LICENSE file
Patterns extracted from MyBodyWatch, a production iOS app.
Extraction assistance by Claude (Anthropic) with human-guided oversight by @cliffordh.