Youll
Architecture

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:

  1. AppDelegate (application(_:didFinishLaunchingWithOptions:)) registers singletons, configures settings, and initializes the networking layer.
  2. SceneDelegate (scene(_:willConnectTo:options:)) creates module instances, injects them into AppCoordinator, 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.

OrderCallRegistersCustomer Implements
1Client.setup(with:)Client.Keys singletonClientKeysType protocol (API URLs, app store ID, deeplink prefixes, SDK keys)
2Core.setup(with:)Core.Keys singletonCoreKeysType protocol (subset of Keys used by Core)
3Client.setupScreenConfigFactory(with:)Client.ScreenConfig singletonScreenConfigFactoryType (~20 sub-factory protocols for per-screen UI config)
4Client.setupUIConfigurator(with:)Client.UIConfigurator singletonUIConfig protocol (button styling, navigation bars, gradients, toast colors)
5Client.AnalyticsManager.setup(with:)Client.AnalyticsManager singletonAnalyticsManagerType 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 = false

Key settings that affect bootstrap behavior:

SettingDefaultEffect
onboardingType.regularControls auth flow: .regular (onboarding then login), .dismissable (anonymous login, onboarding shown modally), .none (no onboarding)
waitForRemoteConfigValuesRefreshOnFirstAppStartfalseWhen true, the app waits on the splash screen until Firebase Remote Config completes its initial fetch
presentSubscriptionScreenAfterLoginfalseShows a paywall immediately after successful login
presentSubscriptionScreenAlwaysOnAppStartfalseShows a paywall on every app start for unsubscribed users
appFirstTimeOpenedtrueManaged 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:

ParameterTypePurpose
environmentBricksNetwork.EnvironmentAPI URLs for GraphQL and REST endpoints (staging vs production)
facebookAuthenticatorFacebookAuthenticatoryType?Optional Facebook OAuth handler. Pass nil if not using Facebook login.
enableRestoreAccountBool (default: false)Controls first-launch session behavior (see warning below)

What Config.init() does internally:

  1. Creates BricksNetwork (GraphQL + REST networking layer via Apollo)
  2. Creates UserService (user state management, profile fetching)
  3. Adds a Firebase Auth token listener that refreshes the current user when a new token arrives
  4. First-launch session handling: if Settings.appFirstTimeOpened == true and enableRestoreAccount == false, calls logout() to destroy any existing session
  5. Configures NotificationManager with a reference to Config
  6. Updates Device.updateKeyWindow() on the main actor
  7. Calls prepareCurrentUser() (sets up analytics user ID tracking via Combine)
  8. Calls prepareCrashlyticsLoggers() (Crashlytics log forwarding)
  9. Increments Settings.appStarts if the user is logged in and not anonymous
  10. 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

SDKRequiredNotes
FirebaseYesMust be first call in AppDelegate
Facebook SDKNoApplicationDelegate.shared.application(...) for OAuth and analytics
AppsFlyerNoAttribution tracking, configurable per customer
BrazeNoPush notifications and in-app messaging
PulseNoNetwork 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.user to configure analytics and prepare IoT controllers when the user changes
  • Notification authorization observer: watches UserNotificationManager.shared.notificationAuthorizationPublisher to gate certain SDK initialization on permission status
  • Tracking authorization observer: watches TrackingManager.shared.trackingAuthorizationPublisher for 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 = sceneWindow

Background 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:

ModuleDependenciesWhen to Include
TuyaModuleScreen config factory for Tuya UIIoT device control apps
TimerModuleScreen config factory, AnalyticsManager.shared, config.requiredNetworkCheckPublisher, ContentService(network: config.bricksNetwork), config.userServiceApps with meditation/session timers
BiometricsModuleScreen config factory, UIConfigurator.shared, config.userService, Apple Health delegateApps with HealthKit integration
CommunityModuleDependencies vary by implementationApps 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:

  1. Sets up LinearJourneyManager for journey progress tracking
  2. Prepares static coordinators: SubscriptionCoordinator, SearchCoordinator, QuizCoordinator, ContentDetailsCoordinator
  3. Checks for restore account scenario (first launch + logged in + restore config exists)
  4. Checks for anonymous user with regular onboarding (triggers logout)
  5. Calls displayFirstScreen() (see next section)
  6. Sets timer module delegate for content detail navigation
  7. Adds Combine observers for deeplinks, app config, user changes, and flag refresh
  8. Calls config.getAppConfig() to fetch remote configuration from the backend

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:

ConditionSet ByDefault
didFinishSplashVideoSplash video completion or backgroundingtrue if showSplashVideo: false
splashScreenController.endedCustom splash controller's publishertrue if no custom controller provided
!config.appConfig.isEmptygetAppConfig() response or cached configUses BNAppConfig.defaultConfig on failure
didInitialFlagsRefreshFirebase Remote Config fetch completionBypassed if waitForRemoteConfigValuesRefreshOnFirstAppStart is false, or if user is already logged in
!didPassSplashScreenInternal guardPrevents 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

ScenarioHow 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 notificationNotification stored, processed 2 seconds after MainCoordinator is visible
Cold launch from deeplinkURL processed via applicationLaunchPublisher after displayApp() completes
Cold launch from shortcut itemStored and processed in sceneDidBecomeActive
Warm resume (foregrounding)sceneDidBecomeActive calls coordinator.didBecomeActive(), checks app version, notifies MainCoordinator
Restore account flowenableRestoreAccount: true + first launch + logged in: shows restore screen, waits for both app config and user data before proceeding

Ordering Constraints

WhatMust Come AfterWhy
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.* assignmentsFramework setup callsConfig.init() reads Settings values like appFirstTimeOpened
Config.setup()All of the aboveTriggers cascade that accesses Keys, ScreenConfig, AnalyticsManager
Post-Config registrationsConfig.setup()Reference Config.shared
Module instantiationConfig.setup()Modules use config.bricksNetwork, config.userService
coordinator.start()Module instantiationAppCoordinator 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

SymptomLikely CauseFix
Crash on launch with fatalError("Please call setup before accessing ...")Setup call missing or misorderedCheck the ordering constraints table and verify all five setup calls happen before Config.setup()
App stuck on splash screenOne or more displayFirstScreen() conditions not metCheck: is getAppConfig() returning? Is Remote Config completing its fetch? Is the splash video or controller signaling completion?
User unexpectedly logged out on first launchenableRestoreAccount 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 launch2-second delay in processing, or MainCoordinator not yet visibleEnsure the app reaches showContent() before the notification is processed. The delay is a known workaround for coordinator initialization timing.
Images not loading throughout the appMissing WebP codec registration in SceneDelegateAdd enableWebpFormat() (SDWebImage WebP coder setup) before creating the AppCoordinator
Onboarding type wrong on first launchRemote 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 start
  • App/Keys.swift: the Keys struct implementing ClientKeysType and CoreKeysType
  • App/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 pattern
  • Core/Core/App/Keys.swift: Core.setup(with:) and the Core Keys singleton
  • Client/Client/App/Config.swift: Config.setup() and the full initialization cascade
  • Client/Client/App/Config+Actions.swift: getAppConfig(), updateUserDeviceInfo(), Crashlytics loggers
  • Client/Client/App/Config+User.swift: refreshCurrentUser(), BricksNetworkDataSource conformance
  • Client/Client/App/Screen Config/ScreenConfigFactory.swift: screen config factory singleton
  • Client/Client/App/UIConfigurator.swift: UI configurator singleton
  • Client/Client/App/AnalyticsManager.swift: analytics manager singleton
  • Client/Client/Coordinators/AppCoordinator.swift: start(), displayFirstScreen(), displayApp()
  • Core/Core/App/Settings.swift: all Settings properties with @UserDefault defaults

Reading order for understanding the bootstrap:

  1. Customer AppDelegate.swift to see the high-level sequence
  2. Each Bricks singleton file (Keys.swift, ScreenConfigFactory.swift, etc.) to understand what each setup call registers
  3. Config.swift to understand the heavy initializer cascade
  4. AppCoordinator.swift to understand start() and the displayFirstScreen() state machine
  5. Customer SceneDelegate.swift to see module injection

On this page