Documentation

Project Structure

Understanding how Whitehall projects are organized.

File Layout

myapp/
├── whitehall.toml          # Project config
├── theme.css               # Optional: the theme, as CSS custom properties
├── .env                    # Environment vars (base, PUBLIC_* only)
├── .env.local              # Local overrides (gitignored)
├── .env.debug              # Debug overrides
├── .env.release            # Release overrides
├── src/
│   ├── main.wh             # Optional: the shell around the router
│   ├── routes/             # File-based routing
│   │   ├── +screen.wh      # → /
│   │   ├── +layout.wh      # Root layout
│   │   ├── +error.wh       # What renders when a loader below it throws
│   │   ├── +screen.load.wh # Screen loader
│   │   ├── (auth)/         # Route group: shares a layout, adds nothing to the path
│   │   │   └── +layout.wh
│   │   └── user/[id]/
│   │       ├── +screen.wh  # → /user/:id
│   │       ├── +screen.load.wh
│   │       └── +screen.rs  # The screen's logic, in Rust
│   ├── lib/                # Everything else: components, stores, models —
│   │   │                    # organized however the project likes
│   │   ├── components/
│   │   ├── stores/
│   │   └── models/
│   ├── widgets/            # Home screen widgets
│   │   └── +Clock.wh       # Widget (+ prefix)
│   ├── notifications/      # Notification channels
│   │   └── +Alerts@high.wh # Channel with priority
│   ├── receivers/          # Broadcast receivers
│   │   └── +Boot.wh
│   └── android/            # Your own Kotlin, if any
├── i18n/
│   └── en.toml             # Translated strings — see $t
├── static/
│   ├── icon.png            # App icon
│   └── fixtures/           # $import files
├── .whitehall/             # Framework workspace (generated Kotlin & Gradle project)
└── dist/                   # Final deliverables (app-debug.apk, app-release.apk)

The Shell — src/main.wh

Optional. Its body wraps every screen, with the router mounted where <slot /> is — the same move +layout.svelte makes in SvelteKit, one level up:

<App theme={Settings.mode}>
  <Column class="w-full h-full">
    <OfflineBanner />
    <slot />
  </Column>
</App>

Absent, it is <App><slot /></App>: the router is the whole app.

File Naming Conventions

Screens

  • +screen.wh - Screen component
  • +layout.wh - Layout wrapper
  • +screen.load.wh - Screen data loader
  • +layout.load.wh - Layout data loader
  • +error.wh - What renders when a loader below it throws
  • +screen@.wh - Screen with no layouts
  • +screen@root.wh - Screen with only root layout
  • +layout@.wh - A layout that starts a new chain
  • +screen.rs, .c, .cc, .cpp, .zig - The screen's logic in another language
  • (name)/ - A route group: shares a layout, adds nothing to the path
  • money.test.wh - A test. No +, because + means it ships

See Layouts for nesting, groups and the @ forms.

Dynamic Routes

Use [param] folders for dynamic segments:

routes/
├── user/[id]/+screen.wh     # → /user/:id
└── posts/[slug]/+screen.wh  # → /posts/:slug

Access params via $screen.params.id.

Widgets

Widget files must start with + and live in src/widgets/:

src/widgets/
└── +Clock.wh       # Home screen widget

Notifications

Notification channel files use +Name@priority.wh format:

src/notifications/
├── +Alerts@high.wh    # High priority
├── +Messages@default.wh
└── +Updates@low.wh

Two directories mean something to the compiler: src/routes/ (a file there is a screen) and src/lib/ (imported as $lib.…). widgets/, notifications/, receivers/ and wear/ are platform entry points. Any other folder is a package name and nothing more.

Package Mapping

Any directory under src/ maps to a package suffix of the same name — src/lib/ is not special-cased, it is just a directory whose sub-folders are the project's own choice.

PathPackage
src/lib/components/Foo.whapp.lib.components.Foo
src/routes/user/[id]/+screen.whapp.routes.user.id.UserScreen
src/lib/stores/Auth.whapp.lib.stores.Auth
src/lib/models/User.whapp.lib.models.User
src/widgets/+Clock.whapp.widgets.Clock

Import Aliases

Whitehall provides convenient import aliases:

AliasExpands To
$lib.models.Xcom.example.app.lib.models.X
$lib.stores.Xcom.example.app.lib.stores.X
$lib.components.Xcom.example.app.lib.components.X
$nativethis screen's sibling native module (+screen.rs, +screen.zig, …)
$native.codecsrc/lib/codec.rs
$toolthe program this directory was before init --app
$icons.lucide.Heartone icon out of the lucide set
$wear.stores.Xsrc/wear/stores/X.wh

One import per line. $app is reserved for the framework and names nothing today — a literal route string is the one spelling for navigating, so there is no generated routes object to import.

Configuration

The whitehall.toml file configures your project:

name = "my-app"
version = "0.1.0"
package = "com.example.myapp"

[screen]
routes = "src/routes"

[android]
min_sdk = 24
target_sdk = 36

[toolchain]
java = "21"
gradle = "8.4"
agp = "8.2.0"
kotlin = "2.0.0"

[dependencies]
"io.coil-kt/coil-compose" = "2.6.0"

[android.icons]
default = "static/icon-default.png"
dark = "static/icon-dark.png"

See Configuration for the full shape.

Next Steps