Context
Today a member declares its build in several hand-written places. A service has an authoredsrc/main.ts and names its root module in package.json. An app keeps its own Vite, Expo, Metro, Maestro, Wrangler and electron-builder files, and those files repeat facts the member already states, such as its name, its bundle identifier and its targets. A generator cannot rewrite them safely, because people edit them by hand as well.
ADR 0294 gives every member one shape: stackra.config.ts and one root module. This record says how everything else is produced. An author states each fact once, in a typed section of stackra.config.ts when only build tools read it, or in a Config class when the running app reads it. Gen writes every tool file and every runtime entry into .stackra/, which is never committed. The few escape hatches are closed and visible in review.
Decision
-
One app definition per member.
stackra.config.tsat the member root calls onedefine*function:defineApp,defineService,definePackageordefineWorker. The argument is static data (enum values, literals and tokens), never a function or an environment read, so gen reads it from the syntax tree without running it. The member key comes from the package name. There is nokeyoption. -
Identity and targets sit at the top of
defineApp. The call holds the app’sname,scheme,area,envandtargets, then one section per platform or tool it uses:ios,android,expo,eas,maestro,e2e,html,pwa,viteandelectron. A section that no target uses is not written. -
Each tool section has the tool’s shape, minus what Stackra owns. A section is typed as the tool’s own configuration with the keys Stackra owns removed, so setting an owned key is a type error. A
rawkey passes through only the keys that gen does not own. -
Where a fact goes. Apply the test in order:
- The same in every environment and read only by build tools: a
stackra.config.tssection. - Read by code while the app runs: a Config class in
src/config/. This coversdesktop.config.ts(modes and overlays, ADR 0281) andchrome.config.ts(decision 11). Gen copies their start values into the tool files that need them. - Different per environment: the member runway (ADR 0296). Examples are EAS build profiles and channels, public
EXPO_PUBLIC_values and the Sentry project. - An account or vendor identifier, such as the EAS project, the Apple team, the App Store Connect identifier or the Play identifier: the root runway (ADR 0296).
- Derivable from other facts: nobody writes it. Permission text comes from the language files, plugins come from the drivers in use, and icons, splash screens, locales, deep-link domains and versions are derived.
- The same in every environment and read only by build tools: a
-
Gen writes every tool file into
.stackra/. The generated files are the Expo configuration,eas.json, the Metro, Babel and Maestro configuration, Vite, Vitest, Playwright, tsdown, Wrangler, electron-builder,index.html, the per-locale web manifests, the service worker, the icons and splash screens, and one.easignoreat the repository root. Babel is written only when a section adds a plugin. A tool that reads only the member root gets a gitignored shim there (ADR 0294, rule R10). A hand edit to a generated file is overwritten on the next run (ADR 0261). -
Code escapes are closed. Code hatches live only in
vite.extend.ts,expo.extend.ts,metro.extend.tsandelectron.extend.tsat the member root. Each is add-only, opens with aReason:header and is listed instackra.manifest.json. Gen applies the hatch to its own output and fails when any owned key has changed. -
Runtime entries are generated. Gen writes each runtime entry to
.stackra/entry/<target>.ts. The entry callscreateAppfrom the subpath for its target, passes the member’s root module and the generated wiring, and exports the result. No one writesmain.tsor an entry barrel (ADR 0294, rule R12). A service’s entry looks like this: -
createAppreturns the object of its runtime. The return type follows the subpath: -
No service factory. There is no
createServiceand nocreateMicroservice. Whether a member runs alone, in a group or in a monolith is root runway topology (ADR 0296), not code. A package has no runtime entry; a host imports the package’s root module. Tests build the root module withcreateTestingModule(Root)and replace ports with.override(Port, fake), as 0263 decision 12 already says. -
The client mounts what the app contains. On the client,
createAppmounts the context of every config class in the module graph, in graph order. For the platform packages that order is session, tenancy, query, realtime, flags, push, platform, theme, i18n, consent, analytics and telemetry, and then the UI kit with its toasts.Module({ wrap })is the only way to add a provider component, and it is allowed only on an app’s root module.@Providerand@Appare retired. An app’ssrcuses no container API. -
Native chrome is a Config class. The status bar, the system navigation bar, the tab bar and the header are fields of a
ChromeConfigsubclass insrc/config/chrome.config.ts. The shell reads them while the app runs, and gen copies their start values into the Expo configuration. Colours are brand tokens, not literals. -
Tab bar items come from routes, then one menu. A route declares
tab: { order, icon }, and it may override chrome for its own screen. The app may rearrange the items in onemenus/tab-bar.menu.ts, which extendsMenu({ kind: MenuKind.TabBar })and starts fromm.tabs(). The tenant and the active role then adjust the items at run time, which 0273 allows for presentation.MenuKindis a closed set: App, Tray, Dock, Context, Navigation, Sidebar, TabBar and AppShortcuts. Every menu builds its items initems(m)from a closed set of item verbs (route, link, action, shortcut, group, separator, navigation and tabs) with closed options and no CSS. An item’spermissioncontrols access, and itsrolesshape presentation only.
Consequences
- A member’s build is one file to read and, for most changes, one file to edit. A tool setting that no section covers is reached through
raw, and a fact that already appears elsewhere is never written twice. - Nobody edits a tool file by hand, so the files cannot drift from the facts they were made from.
- The cost is an ordering rule. Gen runs before any build or test, as it already must for
.stackra/gen/(ADR 0261). A new tool version may wait for a gen change before a member can use it. - Native chrome has a single home: a screen overrides it in one place, and the tab bar is one menu file.
Enforcement
Nothing generates these files yet:packages/developer holds only a README. Two existing checks still enforce the retired shape until the validator changes:
checkServiceintools/scripts/validate-repository.mjsrequiressrc/main.tsand astackra.modulefield inpackage.json..gitignoreignores.stackra/*but re-includes.stackra/app-manifest.json, so.stackra/is not yet fully ignored.
- planned — STA-339: gen writes the tool files, the runtime entries and the root shims, and
stackra gen --checkruns inpnpm verify. - planned — STA-339: the
tool-configsvalidator check. A tracked tool config inside a member fails; a*.extend.tsoutside the closed set, or without aReason:header, fails; a hatch that touches an owned key fails at gen. - planned — STA-339: the
member-rootvalidator check. A missingstackra.config.tsor root module fails, and so do an authoredsrc/main.tsand an authored package barrel. The docs app and thetier:configpresets are exempt (0294 rule R12). - planned — STA-339: the
entriescheck. Each runtime entry callscreateAppfor its target, and a package’sexportsmatch its entry folders. - planned — STA-339: gen fails when a bundle identifier changes after its first upload, unless an ADR records the change.