Bootstrap Sequence
The initialization order when a customer app launches, covering AppDelegate setup, SceneDelegate module injection, and the conditions required before the first screen appears.
Why Order Matters
Bricks uses a singleton pattern for its core services: Keys, ScreenConfigFactory, UIConfigurator, AnalyticsManager, and Config. Each singleton is registered through a setup() call during app launch. If any singleton is accessed before its setup call, the app crashes with a fatalError and no recovery is possible.
This page is the authoritative reference for the initialization order. Every customer app must follow this sequence exactly.
The bootstrap happens in two phases:
- AppDelegate (
application(_:didFinishLaunchingWithOptions:)) registers singletons, configures settings, and initializes the networking layer. - SceneDelegate (
scene(_:willConnectTo:options:)) creates module instances, injects them intoAppCoordinator, and starts the app.
Phase 1: AppDelegate
Step 1: Firebase Initialization
FirebaseApp.configure()This must be the very first call. Firebase Auth, Remote Config, Analytics, and Crashlytics all depend on it. The call reads from GoogleService-Info.plist, which must be included in the app bundle.
Step 2: Bricks Framework Registration
Five setup calls register the singletons that Bricks needs to operate. All five are mandatory.
| Order | Call | Registers | Customer Implements |
|---|---|---|---|
| 1 | Client.setup(with:) | Client.Keys singleton | ClientKeysType protocol (API URLs, app store ID, deeplink prefixes, SDK keys) |
| 2 | Core.setup(with:) | Core.Keys singleton | CoreKeysType protocol (subset of Keys used by Core) |
| 3 | Client.setupScreenConfigFactory(with:) | Client.ScreenConfig singleton | ScreenConfigFactoryType (~20 sub-factory protocols for per-screen UI config) |
| 4 | Client.setupUIConfigurator(with:) | Client.UIConfigurator singleton | UIConfig protocol (button styling, navigation bars, gradients, toast colors) |
| 5 | Client.AnalyticsManager.setup(with:) | Client.AnalyticsManager singleton | AnalyticsManagerType protocol (event forwarding to Firebase, AppsFlyer, etc.) |
A typical implementation:
// 1. Register API keys and environment URLs
Client.setup(with: YourKeys)
Core.setup(with: YourKeys)
// 2. Register UI configuration
Client.setupScreenConfigFactory(with: YourScreenConfigFactory())
Client.setupUIConfigurator(with: YourUIConfigurator.shared)
// 3. Register analytics forwarding
Client.AnalyticsManager.setup(with: YourAnalytics.shared)The customer app provides a single Keys struct that conforms to both ClientKeysType and CoreKeysType. The same instance is passed to both setup calls.
Each of these singletons guards access with fatalError("Please call setup before accessing ..."). If you skip or misordered any of the five calls, the app will crash the first time Bricks tries to use the missing singleton. There is no graceful fallback.
Step 3: Unit Testing Hook
Client.UnitTesting.shared.setupUnitTestsIfNeeded()This checks for launch arguments used by UI and automation testing. It is safe to call in production builds and does nothing when no test arguments are present.
Step 4: Application Settings
The Settings actor stores persistent per-app configuration via @UserDefault property wrappers. Customer apps assign these values to control which features are active and how they behave.
Settings.appId = "your-app-store-id"
Settings.appName = "Your App Name"
Settings.isSearchEnabled = false
Settings.enableTimerLiveActivity = true
Settings.fetchTimerSessions = true
Settings.displaySessionComplete = false
Settings.displayHomeScreenGreetings = falseKey settings that affect bootstrap behavior:
| Setting | Default | Effect |
|---|---|---|
onboardingType | .regular | Controls auth flow: .regular (onboarding then login), .dismissable (anonymous login, onboarding shown modally), .none (no onboarding) |
waitForRemoteConfigValuesRefreshOnFirstAppStart | false | When true, the app waits on the splash screen until Firebase Remote Config completes its initial fetch |
presentSubscriptionScreenAfterLogin | false | Shows a paywall immediately after successful login |
presentSubscriptionScreenAlwaysOnAppStart | false | Shows a paywall on every app start for unsubscribed users |
appFirstTimeOpened | true | Managed by Bricks. Read by Config.init() to detect first launch |
Settings must be configured before Config.setup(). The Config initializer reads Settings.appFirstTimeOpened, Settings.cachedIDTokenKey, and other values to determine first-launch behavior. If you set these after Config initializes, they will have no effect on the bootstrap flow.
Step 5: Module Pre-Configuration
Optional setup for modules that need initialization before Config:
// If using Biometrics module: configure HealthKit
AppleHealthManager.shared.configureForBiometricsTracking()
// Reset player defaults
Player.Settings.resetPlayerDefaultConfig()These calls are only needed if your app activates the corresponding modules.
Step 6: Config Singleton (The Heavy Initializer)
Client.Config.setup(with: YourEnvironment.current, facebookAuthenticator: nil)This is the most critical setup call. Config.setup() creates the Config singleton, which triggers a cascade of internal initialization.
Parameters:
| Parameter | Type | Purpose |
|---|---|---|
environment | BricksNetwork.Environment | API URLs for GraphQL and REST endpoints (staging vs production) |
facebookAuthenticator | FacebookAuthenticatoryType? | Optional Facebook OAuth handler. Pass nil if not using Facebook login. |
enableRestoreAccount | Bool (default: false) | Controls first-launch session behavior (see warning below) |
What Config.init() does internally:
- Creates
BricksNetwork(GraphQL + REST networking layer via Apollo) - Creates
UserService(user state management, profile fetching) - Adds a Firebase Auth token listener that refreshes the current user when a new token arrives
- First-launch session handling: if
Settings.appFirstTimeOpened == trueandenableRestoreAccount == false, callslogout()to destroy any existing session - Configures
NotificationManagerwith a reference to Config - Updates
Device.updateKeyWindow()on the main actor - Calls
prepareCurrentUser()(sets up analytics user ID tracking via Combine) - Calls
prepareCrashlyticsLoggers()(Crashlytics log forwarding) - Increments
Settings.appStartsif the user is logged in and not anonymous - Stores the device's vendor identifier in Settings
On first launch, if enableRestoreAccount is false (the default), Config automatically logs out any existing session. This destroys the Firebase Auth session, clears cached tokens, removes downloaded content, and resets user state. This is by design to ensure a clean first-launch experience, but it can be surprising if you are not aware of it. Set enableRestoreAccount: true if you want to preserve existing sessions on first launch and show a restore account screen instead.
Step 7: Post-Config Registration
After Config is initialized, you can set additional delegates and configuration:
// Register Apple Health delegate for Biometrics module
Config.shared.appleHealthDelegate = YourAppleHealthManager.shared
// Set custom journey text strings
JourneyItemTexts.setup(with: JourneyItemTexts(
currentJourney: "Current Learning Journey",
journeyCompleted: "Journey Completed"))These must come after Config.setup() because they access Config.shared.
Step 8: Third-Party SDK Initialization
| SDK | Required | Notes |
|---|---|---|
| Firebase | Yes | Must be first call in AppDelegate |
| Facebook SDK | No | ApplicationDelegate.shared.application(...) for OAuth and analytics |
| AppsFlyer | No | Attribution tracking, configurable per customer |
| Braze | No | Push notifications and in-app messaging |
| Pulse | No | Network debugging, #if STAGING only |
Third-party SDKs are initialized after Config because some depend on Config.shared being available (e.g., for user ID tracking).
Build configuration flags control conditional initialization:
#if STAGING: enables Pulse network debugging proxy#if DEBUG: enables development-only settings (e.g., timer loading time overrides)#if LOGS: enables diagnostic shortcut items for log collection
Step 9: Observer Setup
The final step in AppDelegate sets up Combine subscribers for reactive state management:
- User state observer: watches
Config.shared.userService.userto configure analytics and prepare IoT controllers when the user changes - Notification authorization observer: watches
UserNotificationManager.shared.notificationAuthorizationPublisherto gate certain SDK initialization on permission status - Tracking authorization observer: watches
TrackingManager.shared.trackingAuthorizationPublisherfor App Tracking Transparency status
These observers run for the lifetime of the app. See Communication Patterns for details on the publisher patterns used throughout Bricks.
Phase 2: SceneDelegate
Step 1: Window Creation
let sceneWindow = UIWindow(windowScene: windowScene)
sceneWindow.backgroundColor = .black
sceneWindow.overrideUserInterfaceStyle = .dark
window = sceneWindowBackground color and interface style are customer choices. The window is stored as a property on SceneDelegate.
Step 2: Image Format Registration
// Register WebP image codec for SDWebImage
let webPCoder = SDImageWebPCoder.shared
SDImageCodersManager.shared.addCoder(webPCoder)
SDWebImageDownloader.shared.setValue("image/webp,image/*,*/*;q=0.8", forHTTPHeaderField: "Accept")The Youll backend serves images in WebP format. Without this registration, images throughout the app will fail to load.
Step 3: Module Instantiation
Customer apps create instances of only the modules they want to activate. Each module requires specific dependencies:
| Module | Dependencies | When to Include |
|---|---|---|
TuyaModule | Screen config factory for Tuya UI | IoT device control apps |
TimerModule | Screen config factory, AnalyticsManager.shared, config.requiredNetworkCheckPublisher, ContentService(network: config.bricksNetwork), config.userService | Apps with meditation/session timers |
BiometricsModule | Screen config factory, UIConfigurator.shared, config.userService, Apple Health delegate | Apps with HealthKit integration |
CommunityModule | Dependencies vary by implementation | Apps with social features |
All module instantiation must happen after Config.setup(). Modules depend on Config.shared properties like bricksNetwork and userService. If you create a module before Config is initialized, it will crash when accessing these dependencies.
Step 4: AppCoordinator Creation
coordinator = AppCoordinator(
window: sceneWindow,
config: config,
showSplashVideo: false,
splashScreen: YourSplashViewController(),
timerModuleInterface: timerModule, // nil to disable
tuyaModuleInterface: tuyaModule, // nil to disable
biometricsModuleInterface: biometricsModule, // nil to disable
communityModuleInterface: nil // nil to disable
)All module interface parameters are optional. Passing nil means the feature is not available. Bricks hides related UI elements when a module is not injected.
The showSplashVideo parameter controls whether a video splash screen plays on launch. Set to false to skip the video and show a static splash screen instead (provided via splashScreen). You can also provide a splashScreenController conforming to SplashScreenController for custom splash behavior with an ended publisher.
Step 5: coordinator.start()
AppCoordinator.start() kicks off the app launch sequence:
- Sets up
LinearJourneyManagerfor journey progress tracking - Prepares static coordinators:
SubscriptionCoordinator,SearchCoordinator,QuizCoordinator,ContentDetailsCoordinator - Checks for restore account scenario (first launch + logged in + restore config exists)
- Checks for anonymous user with regular onboarding (triggers logout)
- Calls
displayFirstScreen()(see next section) - Sets timer module delegate for content detail navigation
- Adds Combine observers for deeplinks, app config, user changes, and flag refresh
- Calls
config.getAppConfig()to fetch remote configuration from the backend
Step 6: Notification and Deeplink Handling
SceneDelegate handles three types of cold-launch entry points:
Push notifications: If connectionOptions.notificationResponse is present, the notification data is stored and processed with a 2-second delay after MainCoordinator becomes visible. The delay ensures the tab bar and navigation stack are fully initialized before attempting to navigate to the notification's target.
Deeplinks and universal links: SceneDelegate subscribes to config.applicationLaunchPublisher, which fires asynchronously after displayApp() completes. At that point, it processes connectionOptions.urlContexts and connectionOptions.userActivities.
Shortcut items (3D Touch / Quick Actions): Stored from connectionOptions.shortcutItem and processed in sceneDidBecomeActive.
The displayFirstScreen() State Machine
This is the gating logic that determines when the app moves past the splash screen. It is the most common source of "app stuck on splash screen" issues.
All conditions must be true before the first screen appears:
| Condition | Set By | Default |
|---|---|---|
didFinishSplashVideo | Splash video completion or backgrounding | true if showSplashVideo: false |
splashScreenController.ended | Custom splash controller's publisher | true if no custom controller provided |
!config.appConfig.isEmpty | getAppConfig() response or cached config | Uses BNAppConfig.defaultConfig on failure |
didInitialFlagsRefresh | Firebase Remote Config fetch completion | Bypassed if waitForRemoteConfigValuesRefreshOnFirstAppStart is false, or if user is already logged in |
!didPassSplashScreen | Internal guard | Prevents double-navigation |
Once all conditions are met, the flow branches:
displayFirstScreen()
│
├─ Email not verified? → Show verification screen
│ ├─ Verified → displayApp()
│ └─ Dismissed → logout → displayApp()
│
├─ Logged in or anonymous? → showContent()
│ └─ Creates MainCoordinator with injected modules
│
└─ Not logged in? → startGuestFlow()
├─ onboardingType == .regular → Show full auth flow
└─ onboardingType != .regular → Perform anonymous login → showContent()After displayApp() completes, it sends applicationLaunchPublisher to signal that the app is ready for deeplink and notification processing.
Launch Variants
| Scenario | How It Differs |
|---|---|
| Normal cold launch (logged in) | AppDelegate setup, SceneDelegate modules, displayFirstScreen() shows content immediately |
| Normal cold launch (not logged in) | Same setup, then guest flow (onboarding or anonymous login depending on onboardingType) |
| First launch (new install) | Settings.appFirstTimeOpened is true. Config auto-logs out unless enableRestoreAccount is true. May wait for Remote Config on splash if waitForRemoteConfigValuesRefreshOnFirstAppStart is true. |
| Cold launch from push notification | Notification stored, processed 2 seconds after MainCoordinator is visible |
| Cold launch from deeplink | URL processed via applicationLaunchPublisher after displayApp() completes |
| Cold launch from shortcut item | Stored and processed in sceneDidBecomeActive |
| Warm resume (foregrounding) | sceneDidBecomeActive calls coordinator.didBecomeActive(), checks app version, notifies MainCoordinator |
| Restore account flow | enableRestoreAccount: true + first launch + logged in: shows restore screen, waits for both app config and user data before proceeding |
Ordering Constraints
| What | Must Come After | Why |
|---|---|---|
FirebaseApp.configure() | Nothing (first call) | All Firebase services depend on it |
Client.setup() | FirebaseApp.configure() | Keys may reference Firebase-dependent values |
Core.setup() | FirebaseApp.configure() | Same as above |
setupScreenConfigFactory() | Client.setup() | Factory may reference Client internals |
setupUIConfigurator() | Client.setup() | UI config may reference Client internals |
AnalyticsManager.setup() | Client.setup() | Config triggers analytics events during init; AnalyticsManager must be ready |
Settings.* assignments | Framework setup calls | Config.init() reads Settings values like appFirstTimeOpened |
Config.setup() | All of the above | Triggers cascade that accesses Keys, ScreenConfig, AnalyticsManager |
| Post-Config registrations | Config.setup() | Reference Config.shared |
| Module instantiation | Config.setup() | Modules use config.bricksNetwork, config.userService |
coordinator.start() | Module instantiation | AppCoordinator uses injected modules |
Thread Safety
All setup calls must happen on the main thread during application(_:didFinishLaunchingWithOptions:). The singletons use nonisolated(unsafe) module-level variables, which means they have no built-in thread safety. Thread safety during bootstrap is guaranteed by the UIKit application lifecycle: didFinishLaunchingWithOptions always runs on the main thread, and SceneDelegate callbacks follow the same rule.
Do not call any setup functions from background threads or dispatch queues.
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
Crash on launch with fatalError("Please call setup before accessing ...") | Setup call missing or misordered | Check the ordering constraints table and verify all five setup calls happen before Config.setup() |
| App stuck on splash screen | One or more displayFirstScreen() conditions not met | Check: is getAppConfig() returning? Is Remote Config completing its fetch? Is the splash video or controller signaling completion? |
| User unexpectedly logged out on first launch | enableRestoreAccount is false (default) | This is expected behavior. Set enableRestoreAccount: true to preserve sessions, or document the expected first-launch behavior for your team. |
| Push notification not handled on cold launch | 2-second delay in processing, or MainCoordinator not yet visible | Ensure the app reaches showContent() before the notification is processed. The delay is a known workaround for coordinator initialization timing. |
| Images not loading throughout the app | Missing WebP codec registration in SceneDelegate | Add enableWebpFormat() (SDWebImage WebP coder setup) before creating the AppCoordinator |
| Onboarding type wrong on first launch | Remote Config not fetched before displayFirstScreen() | Set Settings.waitForRemoteConfigValuesRefreshOnFirstAppStart = true to wait for config, or set Settings.onboardingType explicitly |
Minimal Bootstrap Template
A minimal AppDelegate and SceneDelegate for a new customer app:
// AppDelegate.swift
import UIKit
import Client
import Core
import FirebaseCore
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 1. Firebase (must be first)
FirebaseApp.configure()
// 2. Register Bricks singletons
Client.setup(with: YourKeys)
Core.setup(with: YourKeys)
Client.setupScreenConfigFactory(with: YourScreenConfigFactory())
Client.setupUIConfigurator(with: YourUIConfigurator.shared)
Client.AnalyticsManager.setup(with: YourAnalytics.shared)
// 3. Application settings
Settings.appId = "your-app-store-id"
Settings.appName = "Your App"
// 4. Config singleton (must come after all setup calls and settings)
Client.Config.setup(with: YourEnvironment.current, facebookAuthenticator: nil)
return true
}
}// SceneDelegate.swift
import UIKit
import Client
import Core
import SDWebImage
import SDWebImageWebPCoder
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var coordinator: AppCoordinator!
private lazy var config = Config.shared
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
// 1. WebP image support
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
// 2. Create window
let sceneWindow = UIWindow(windowScene: windowScene)
window = sceneWindow
// 3. Create AppCoordinator (pass nil for unused modules)
coordinator = AppCoordinator(
window: sceneWindow,
config: config,
showSplashVideo: false,
splashScreen: YourSplashViewController())
// 4. Launch the app
coordinator.start()
}
}This template omits optional modules (Timer, Tuya, Biometrics, Community), third-party SDKs beyond Firebase, and observer setup. Add these incrementally as your app requires them.
What's Next
For Agents
When working with bootstrap code, read files in this order:
Customer app layer:
AppDelegate.swift: the full initialization sequence (Steps 1-9)SceneDelegate.swift: module injection and coordinator startApp/Keys.swift: the Keys struct implementingClientKeysTypeandCoreKeysTypeApp/AppEnvironment.swift: API environment URLs (staging vs production)
Bricks layer (where setup functions are defined):
Client/Client/App/Keys.swift:Client.setup(with:)and the Keys singleton patternCore/Core/App/Keys.swift:Core.setup(with:)and the Core Keys singletonClient/Client/App/Config.swift:Config.setup()and the full initialization cascadeClient/Client/App/Config+Actions.swift:getAppConfig(),updateUserDeviceInfo(), Crashlytics loggersClient/Client/App/Config+User.swift:refreshCurrentUser(),BricksNetworkDataSourceconformanceClient/Client/App/Screen Config/ScreenConfigFactory.swift: screen config factory singletonClient/Client/App/UIConfigurator.swift: UI configurator singletonClient/Client/App/AnalyticsManager.swift: analytics manager singletonClient/Client/Coordinators/AppCoordinator.swift:start(),displayFirstScreen(),displayApp()Core/Core/App/Settings.swift: all Settings properties with@UserDefaultdefaults
Reading order for understanding the bootstrap:
- Customer
AppDelegate.swiftto see the high-level sequence - Each Bricks singleton file (
Keys.swift,ScreenConfigFactory.swift, etc.) to understand what each setup call registers Config.swiftto understand the heavy initializer cascadeAppCoordinator.swiftto understandstart()and thedisplayFirstScreen()state machine- Customer
SceneDelegate.swiftto see module injection