Skip to content
Back to skills

App Startup And Bootstrap

ASecurity

Orders an Expo app's cold start - polyfills, error net, splash hold, storage open, providers, splash release, background flush. Use when editing the root _layout.tsx or the app root, or the splash hangs or flashes. Not for routes (navigation-and-routing).

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 18, 2026
developmentgoreactnodedatabaseperformance

Works with

  • cli

Security analysis

A100/100

Pro scans all 19 files and shows the line behind each finding

Scanned October 18, 2026

npx -y skills add zakariaf/ReactNative-Skills --skill app-startup-and-bootstrap --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of App Startup And Bootstrap?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for App Startup And Bootstrap
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zakariaf-app-startup-and-bootstrap-reactnative-skills/badge)](https://www.skillsdirectory.com/skills/zakariaf-app-startup-and-bootstrap-reactnative-skills)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: app-startup-and-bootstrap
description: Orders an Expo app's cold start - polyfills, error net, splash hold, storage open, providers, splash release, background flush. Use when editing the root _layout.tsx or the app root, or the splash hangs or flashes. Not for routes (navigation-and-routing).
---

# App startup and bootstrap

The app starts in one fixed order: the entry loads the polyfills and then the router; the root layout
installs the error net, holds the native splash and decides the direction before anything opens; the
app root opens storage once, connects the error log and hydrates the stores synchronously, mounts the
providers in dependency order and releases the splash once the first screen is real. Buffered writes
flush from one root hook when the app leaves the foreground. Optional kits (crash reporting,
analytics, notifications, ...) plug into typed slots from one registry, so the root files stay
canonical when a kit is added. One zero-dependency checker proves it.

## Rules that must hold

1. **The entry imports the polyfills first and `expo-router/entry` last.** Nothing follows the router
   import. Why: ES imports evaluate in order; a module after the router entry loads after the root
   component was registered. (`entry-router-last`)
2. **The root layout's first imports are side effects: the error net installer, then the splash
   hold.** Every import with bindings comes after them. Why: a module that throws while loading must
   find the net, and the hold must run before any other app module. (`root-import-order`)
3. **The native splash is held once, at module scope, in `hold-splash.ts`.** Why: Expo Router hides
   an unheld splash at "navigation ready", over a direction restart's empty screen or a blocking wait;
   called in a component or effect, the hold comes after native content appeared, when iOS and Android
   may already have hidden it. (`splash-hold`)
4. **The splash is released once, in an effect, below the direction gate, when the startup gate is
   ready** (`useStartupReady(gate, hideSplash)` in the app root). A startup component renders `null`
   only while the splash is held. Why: a hold with no release keeps a release build on the splash
   forever; a release in render or above the gate uncovers a frame that is not the first screen.
   (`splash-release`, `blank-frame`)
5. **The root layout's first render creates adapters, reads config and resolves the language, and
   opens nothing; `DirectionGate` is its outermost element (directly inside web-support's
   `WebStorageGate` when the app ships the web); everything with effects lives in its child
   `AppRoot`.** Why: a direction restart re-runs every module and initializer, so work done
   before the decision happens twice (rtl-and-direction's `direction-before-render` enforces the gate).
6. **The data layer opens once, synchronously, in the app root's startup initializer, then the error
   net's sink connects, then the query client, settings and stores hydrate, then the optional kits are
   created, in that order** (`startApp`, run as `useState(() => startApp(services, language))`). Why: the first frame has the real rows
   with no loading state, the buffered errors of the first moments reach the log before any provider
   renders, and a second open runs migrations again. (`storage-open-once`, `sink-before-providers`)
7. **The startup path awaits nothing.** The root layout and the app root contain no `await`,
   `.then`, `.catch` or `.finally`; async work the first screen truly needs is a non-interactive
   blocking task of `createStartupGate` with a literal `deadlineMs` at or under 3000. A session
   restore follows auth-and-secure-storage: its read is async and non-interactive, so it is a blocking
   task under the deadline, and a miss starts the session as unknown or unavailable; a biometric
   unlock is never a gate task but a lock screen after the splash. Why: a promise that never settles
   keeps a held splash up forever, and a prompt that waits for the person outlasts any deadline; the
   gate always opens by its deadline and reports what it missed. (`startup-await`, `startup-deadline`)
8. **Fonts are embedded with the expo-font config plugin, never loaded at runtime.** Why: `useFonts`
   either holds the first frame or swaps the font after it. (`runtime-font-load`)
9. **Providers nest in one dependency order and each is mounted once on the root path:** services (or
   `AppProviders`), data, query, settings store, stores, preferences (i18n, theme, text locale), root
   stack (gesture root, keyboard, `RootMotion` (the one MotionConfig), navigation theme, session,
   feedback, ads, games, then `AppStack`, whose `RootKits` slot wraps the `Stack` in the kits'
   providers). Why: each provider reads only those
   outside it, a misplaced one throws only when its path renders on a device, and a second copy
   silently overrides the first. (`provider-order`, `provider-once`)
10. **Deferred work starts after the splash release, in the component or service that owns it; never
    through `InteractionManager`.** Why: consent, entitlements, OTA checks and prefetches must not delay
    the first frame, and `InteractionManager` is a deprecated stub in React Native 0.86 that runs on the
    next tick. (`interaction-manager`)
11. **Buffered writes flush from one root hook when the app leaves the foreground:**
    `useFlushOnBackground(flushers, errorLog)` once, in the app root, before
    `useCheckpointOnBackground`; features hand it a `Flusher` and never write from their own `AppState`
    listener. Why: an iOS swipe-kill from the app switcher sends `inactive` and no `background`, and
    listeners run in subscription order, so the drafts must land before the WAL is folded.
    (`flush-root`, `lifecycle-write`)
12. **Optional kits join through the slots, never through the root files.** A kit returns a `RootKit`
    (`forward`, `blocking`, `flushers`, `motion`, `opensDebugRoutes`, `provide`, `effects`; every part
    optional) from the app's one registry, `src/services/create-root-kits.ts`; `startApp` first calls
    the registry's `prepareRootKits` (the ports every reader uses: a test build's simulated clock and
    network), then `createRootKits` once, after the stores, attaches the kits to the error sink
    (which replays the reports it kept) and hands their tasks to the gate and their flushers to the
    flush; `RootMotion` renders the first kit's motion slot in `MotionConfig`'s place; `AppStack`
    opens its `(debug)` group only while a kit says so and renders `<RootKits kits>` around the
    `Stack`; a kit that needs Metro lists a `MetroKit` in `scripts/metro/metro-kits.ts`, which the
    canonical `metro.config.js` composes. A slot no kit fills does nothing, and a test build's kits
    are reached only through the test-only gate. Why: the root skeleton stays the canonical file
    every skill cites, a kit is added or removed in one line, and every kit's work still runs at its
    place in the startup order (E1, E2, E6, E7, F5, F6, I1). (`root-kits`)
13. **Prove the launch on a release build, on both platforms.** Expo Go shows the app icon instead of
    the splash, and a development build does not apply every splash config property (Expo's SDK 57
    splash-screen docs); only a release build shows the hold, the release and a direction restart as
    people see them.

## Workflow

1. **Read the order**: [references/startup-order.md](references/startup-order.md) (section 1 is the
   table of every step, its file and its owner skill). Find where the change belongs before writing it.
2. **New app or no root yet**: copy the templates (line 1 names each repo path):
   `templates/src/app/_layout.tsx`, `templates/src/features/root-navigator/` (`app-root.tsx`,
   `root-stack.tsx`, `app-stack.tsx`, `hold-splash.ts`, `startup-gate.ts`, `use-startup-ready.ts`,
   `background-flush.ts`, `use-flush-on-background.ts`, `root-kits.tsx`, the helpers each with its
   test), `templates/src/services/root-kits/root-kit.ts` (+ test, the slot types and helpers),
   `templates/src/services/create-root-kits.ts` (the app's registry, empty) and
   `templates/contracts/startup.json`. These files are the canonical root skeleton that other skills
   cite; each of them owns one piece inside it. They expect the composition root
   (architecture-and-boundaries), the error net (error-handling-typed-results), `DirectionGate`
   (rtl-and-direction), the data layer (persistence-and-migrations), `watchSystemA11y` and
   `MotionConfig` (accessibility-as-code), `useScreenAnimation` (motion-and-animation) and the
   navigation theme (design-system-structure); set up a missing one with its skill first. Route groups
   and `Stack.Protected` inside `AppStack` are navigation-and-routing's.
3. **Adding a piece of startup.** An optional kit from another skill (crash reporting, analytics,
   push or local notifications): one entry in `src/services/create-root-kits.ts`, as that skill's
   example shows; the root files stay unchanged (read [references/startup-order.md](references/startup-order.md)
   section 15, the slot contract). An app piece (a store, a client, the session): its creation in
   `startApp` at its row of the order table, its provider at its place in the tree, and its name in
   `appRootOrder` in `contracts/startup.json` if it is new; pass what a kit reads (the settings, the
   session) on to `createRootKits`. Imitate [examples/app-root-with-session.tsx](examples/app-root-with-session.tsx).
4. **Work that is async**: decide with [references/startup-gate-and-deferred-work.md](references/startup-gate-and-deferred-work.md)
   (section 1) whether it blocks the first screen. Deferred is the default; a blocking task returns a
   Result, goes into `createStartupGate({ blocking })` and gets a test that drives it resolving, failing
   and hanging.
5. **Splash problems** (hangs, flashes, empty frame): [references/splash-screen.md](references/splash-screen.md)
   (sections 3-4 and the pitfalls table). The splash art itself is icons-app-icon-and-splash's.
6. **A buffered writer** (debounced draft, batch queue): read [references/lifecycle-flush.md](references/lifecycle-flush.md),
   imitate [examples/task-draft-buffer.ts](examples/task-draft-buffer.ts), return its flusher from
   `startApp`.
7. **Test**: `npx jest src/features/root-navigator --ci --maxWorkers=2`, then `npm run typecheck` and
   `npx eslint . --max-warnings 0`.
8. **Run the checker** from the repo root: `node ${CLAUDE_SKILL_DIR}/scripts/check-startup.mjs .`;
   look up a FAIL line's rule in [references/checker-rules.md](references/checker-rules.md); fix and
   rerun until `RESULT: PASS`.
9. **On devices** (by hand, once per release candidate, a step for the developer): cold start a release
   build on a named simulator and a named emulator, once per layout direction of the app's languages:
   the frame after the splash is the first screen; type a draft, swipe the app away, relaunch: the draft
   is there.

## Definition of done

- [ ] `index.ts` imports the polyfills first and `expo-router/entry` last (or `main` is `expo-router/entry`).
- [ ] `src/app/_layout.tsx` imports the error net installer, then `hold-splash`; renders `DirectionGate` outermost (inside `WebStorageGate` when the app ships the web) and `AppRoot` inside it; opens nothing.
- [ ] `AppRoot` runs `startApp` once in a `useState` initializer: the registry's prepared ports, open, sink, client, settings, stores, kits, gate; no `await` or promise chain; `ServicesProvider` gets the prepared services.
- [ ] The splash is held once at module scope and released once through `useStartupReady`; every blocking task returns a Result and the gate's deadline is at most 3000 ms.
- [ ] Providers follow `appRootOrder` / `rootStackOrder`, each mounted once; fonts are embedded; no `InteractionManager`.
- [ ] Buffered writers hand a `Flusher` to the one `useFlushOnBackground`, called before `useCheckpointOnBackground`.
- [ ] Optional kits are entries of `createRootKits`, created once in `startApp` after the stores and handed to every slot (`errors.attach`, `kitBlocking`, `kitFlushers`, `RootMotion`, `AppStack`'s `(debug)` guard, `<RootKits kits>`); `prepareRootKits` runs before the data layer opens; no root file imports a kit; Metro kits live in `scripts/metro/metro-kits.ts`.
- [ ] `npx jest src/features/root-navigator --ci --maxWorkers=2`, `npm run typecheck` and `npx eslint . --max-warnings 0` pass.
- [ ] The release-build launch of step 9 was done on iOS and Android, or listed for the developer.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-startup.mjs .` prints `RESULT: PASS` (no SKIP lines are expected: every file it reads is this skill's).

## Anti-patterns

- **`await openDatabaseAsync()` in the root layout behind a loading spinner** - a second startup path
  with nothing on screen. Open synchronously in `startApp`.
- **`SplashScreen.preventAutoHideAsync()` inside `useEffect`** - by then the splash may be gone. Hold
  it at module scope in `hold-splash.ts`.
- **`hide()` in the root layout's effect** - it also runs in the JS run that is about to restart for
  the direction. Release in the app root.
- **A hold and no release "because Expo Router hides it"** - the router leaves a held splash alone.
- **`useFonts` in the root layout with `if (!loaded) return null`** - an empty frame or a font swap.
  Embed the fonts.
- **`InteractionManager.runAfterInteractions(prefetch)`** - runs on the next tick, mid-startup. Start
  it from the owner after the release.
- **`appErrorNet.connect` in `useEffect`** - errors of the first render wait or are lost. Connect in
  `startApp`.
- **A screen's own `AppState` listener that saves its draft** - gone when the screen unmounts, and
  unordered against the checkpoint. Hand a `Flusher` to the root.
- **Flushing only on `background`** - an iOS swipe-kill from the switcher never sends it.
- **A `MotionConfig` in `PreferencesRoot` beside the root stack's `RootMotion`, or a second
  `I18nProvider`** - the inner one overrides the person's setting (and two MotionConfigs fight over
  Reanimated's global mode). `PreferencesRoot` provides `ReduceMotionPreferenceContext` instead.
- **`<StrictMode>` around `AppRoot`** - React runs the initializer twice in development: two database
  opens. Put it around `RootStack` if wanted.
- **A blocking task without a deadline, or a deadline from a variable** - a splash that can wait
  forever. A literal or module constant up to 3000 ms.
- **A biometric read (`requireAuthentication`) as a startup-gate task** - the deadline expires under
  the Face ID or BiometricPrompt sheet and the answer arrives after the app opened. Show a lock
  screen after the splash release.
- **`reloadAppAsync` in a module body** - crashed a Release build while the bundle was still
  evaluating. Restart from a mounted component's effect.
- **A kit wired into `app-root.tsx`, `app-stack.tsx` or `metro.config.js`** (a Sentry sink in
  `startApp`, a tap hook in `AppStack`, `getSentryExpoConfig` in Metro) - forks the canonical skeleton
  every skill cites. Register the kit in `createRootKits`; list a Metro kit in `metro-kits.ts`.

## Files in this skill

| File | What it is | Read/run when |
|---|---|---|
| [references/startup-order.md](references/startup-order.md) | The canonical order step by step (entry, imports, first render, gate, startApp, hooks, providers, splash, deferred, flush), owners, reloads, Strict Mode, React Navigation and multi-package notes | Workflow steps 1-3 |
| [references/splash-screen.md](references/splash-screen.md) | Verified splash behaviour of expo-splash-screen and Expo Router on iOS and Android, hold and release, crashes, Expo Go, pitfalls, tests | Workflow step 5 |
| [references/startup-gate-and-deferred-work.md](references/startup-gate-and-deferred-work.md) | Synchronous vs blocking vs deferred work, the gate, the deadline, where deferred work starts, fonts, InteractionManager, cold start hand-off | Workflow step 4 |
| [references/lifecycle-flush.md](references/lifecycle-flush.md) | AppState sequences per platform, what must flush, one root hook, flush before checkpoint, synchronous flushers, tests | Workflow step 6 |
| [references/checker-rules.md](references/checker-rules.md) | Every check-startup rule, the contract keys, SKIP and NOT APPLICABLE, limits | Workflow step 8, on any FAIL line |
| `templates/src/app/_layout.tsx` | The root layout: net, hold, `ErrorBoundary`, adapters and language, `DirectionGate` around `AppRoot` | Workflow step 2 |
| `templates/src/features/root-navigator/app-root.tsx` | The app root: `startApp`, lifecycle hooks, splash release, providers | Workflow steps 2-3 |
| `templates/src/features/root-navigator/root-stack.tsx` | The root stack: gesture root, `KeyboardProvider`, `RootMotion`, navigation theme around `AppStack` | Workflow steps 2-3 |
| `templates/src/features/root-navigator/root-motion.tsx` | `RootMotion`: the one `MotionConfig` with the preference above it, or the first kit's motion slot | Workflow steps 2-3 |
| `templates/src/features/root-navigator/root-motion.test.tsx` | Its tests: "system" by default, the provided preference, the first kit's motion slot | Workflow step 2 |
| `templates/src/features/root-navigator/app-stack.tsx` | The root `Stack` with `useScreenAnimation()`, the test-build `(debug)` group behind `Stack.Protected` (`opensDebugRoutes`), inside the `RootKits` slot; route groups go inside it | Workflow step 2 |
| `templates/src/features/root-navigator/root-kits.tsx` | `RootKits`: every kit's provider around the Stack and its effects beside it | Workflow steps 2-3 |
| `templates/src/features/root-navigator/root-kits.test.tsx` | Its tests: no kits, provider nesting in registry order, effects mounted once | Workflow step 2 |
| `templates/src/services/root-kits/root-kit.ts` | The slot contract: `RootKit`, `createKitErrorSink` (records, then forwards; replays the first moments), `kitBlocking`, `kitFlushers`, `kitMotion`, `kitsOpenDebugRoutes` | Workflow steps 2-3 |
| `templates/src/services/root-kits/root-kit.test.ts` | Its tests: records always, replay, limit, attach once, a throwing kit, shared names | Workflow step 2 |
| `templates/src/services/create-root-kits.ts` | The app's one kit registry: `prepareRootKits` (returns the services unchanged) and `createRootKits` (empty); part of the composition root | Workflow steps 2-3 |
| `templates/src/features/root-navigator/hold-splash.ts` | The module-scope splash hold | Workflow step 2 |
| `templates/src/features/root-navigator/hold-splash.test.ts` | Its test: once per registry, rejection to the net | Workflow step 2 |
| `templates/src/features/root-navigator/startup-gate.ts` | `createStartupGate`: blocking tasks under one deadline, never rejects | Workflow steps 2 and 4 |
| `templates/src/features/root-navigator/startup-gate.test.ts` | Its tests: no task, all tasks, failure, hang, throw, start twice, unsubscribe, a throwing listener | Workflow step 4 |
| `templates/src/features/root-navigator/use-startup-ready.ts` | Reads the gate and releases the splash after the commit | Workflow step 2 |
| `templates/src/features/root-navigator/use-startup-ready.test.ts` | Its tests with a real gate | Workflow step 2 |
| `templates/src/features/root-navigator/background-flush.ts` | `Flusher`, `isLeavingForeground`, `runFlushers` | Workflow step 6 |
| `templates/src/features/root-navigator/background-flush.test.ts` | The transition table and the throwing flusher | Workflow step 6 |
| `templates/src/features/root-navigator/use-flush-on-background.ts` | The one root flush hook | Workflow step 6 |
| `templates/src/features/root-navigator/use-flush-on-background.test.ts` | iOS and Android sequences, unmount | Workflow step 6 |
| `templates/contracts/startup.json` | The app's startup facts for the checker (defaults filled in) | Workflow steps 2-3 |
| `templates/contracts/startup.schema.json` | Its JSON schema | When editing the contract |
| [examples/app-root-with-session.tsx](examples/app-root-with-session.tsx) | Shape: the app root with the session restore (auth-and-secure-storage) as its one blocking task, settings, stores, query and a draft buffer | Workflow step 3 |
| [examples/task-draft-buffer.ts](examples/task-draft-buffer.ts) | Shape: a debounced writer that hands a `Flusher` to the root | Workflow step 6 |
| `scripts/check-startup.mjs` | The checker (16 rules) | Workflow step 8, definition of done |
| `scripts/selftest.mjs` | Proves the checker on the fixtures | After changing the checker |
| `scripts/check-lib.mjs` | Shared checker library (synced; do not edit here) | Never by hand |
| `scripts/lib/source-scan.mjs` | Shared source lexer and import resolver (synced; do not edit here) | Never by hand |
| `scripts/lib/startup-scan.mjs` | Scope, function and JSX-span analysis for the checker | When changing the checker |
| `assets/shared.json` | Which shared files this skill syncs | Maintainers |
| `tests/` | `bases/app/` (stand-ins for the error net and the entry) and `fixtures/check-startup/` (good, bad-*, pass-*, error-* cases on the templates) | After changing the checker or a template |

## Related skills

- `navigation-and-routing` - the routes, route groups, guards and `Stack` options inside `AppStack`.
- `rtl-and-direction` - `DirectionGate` and the direction restart this order is built around.
- `error-handling-typed-results` - the error net and the root `ErrorBoundary`.
- `persistence-and-migrations` - what `openDeviceData` opens, migrations and the WAL checkpoint.
- `architecture-and-boundaries` - the composition root and "nothing at import time".
- `settings-and-preferences`, `state-management-zustand`, `networking-and-server-state` - the stores, `PreferencesRoot` and `QueryProvider` that join `startApp`.
- `i18n-strings-and-catalogs` - the Intl polyfills and language resolution.
- `icons-app-icon-and-splash` - the splash image and colours.
- `performance-budgets` - measuring cold start.
- `ota-updates`, `admob-ads-and-consent`, `in-app-purchases` - deferred startup work.
- `deep-links-and-universal-links`, `push-notifications`, `local-notifications` - the incoming-link gate and the tap hooks that run below the navigator (startup-order step F6; the notification kits plug into the slots).
- `crash-reporting-and-observability`, `analytics-and-consent` - root kits: error forwarding, consent after the splash, screen views, the Metro kit.
- `app-variants-and-environment` - the canonical `metro.config.js` that composes the Metro slot.

Files in this skill

  • SKILL.md22 KB
  • assets/shared.json454 B
  • examples/app-root-with-session.tsx6.1 KB
  • examples/task-draft-buffer.ts2.4 KB
  • references/checker-rules.md9.8 KB
  • references/lifecycle-flush.md6.9 KB
  • references/splash-screen.md11 KB
  • references/startup-gate-and-deferred-work.md10.2 KB
  • references/startup-order.md24.4 KB
  • scripts/check-startup.mjs48.3 KB
  • scripts/lib/source-scan.mjs35.1 KB
  • scripts/lib/startup-scan.mjs7.3 KB
  • scripts/selftest.mjs437 B
  • templates/contracts/startup.json1 KB
  • templates/contracts/startup.schema.json2.8 KB
  • templates/src/app/_layout.tsx3.5 KB
  • templates/src/features/root-navigator/app-root.tsx6.4 KB
  • templates/src/features/root-navigator/app-stack.tsx2.5 KB
  • templates/src/features/root-navigator/background-flush.test.ts1.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.