beej
बीज — plant a new Flutter app.
A Dart CLI that generates a complete, buildable Flutter project: material_ui,
Riverpod, routing, localization, theming with live personalization, an About
module, release tooling and agent config — with the parts that genuinely differ
left as explicit choices.
It is opinionated rather than general-purpose. The opinions came from thirteen
production Flutter apps that had converged on one stack but drifted into five
lib/ layouts, four analyzer configs and four localization setups. Defaults are
neutral (com.example, English), and beej config is where you make them
yours.
dart pub global activate beej
beej config set org com.acme # once, not per project
beej create tipot # interactive
beej create tipot --yes # take every default
beej create --spec app.yaml # from a file
beej create tipot --yes --dry-run # show the plan, write nothing
What you always get
-
material_ui— Material decoupled from the Flutter SDK. Nothing underlib/importspackage:flutter/material.dart, and a generated test fails the build if that ever changes. -
Riverpod 3, no code generation.
-
Localization in
lib/l10n/— English and Nepali, actually translated. -
Theming from design tokens, with working personalization: accent colour, theme mode, language and text size, persisted and applied before the first frame.
-
The About module — privacy policy, open-source licences, share, rate, more apps, contact support — wired into routes and the settings list.
-
Error capture —
FlutterError.onError,PlatformDispatcher.onErrorand a guarded zone aroundmain, feeding a deduplicated, size-capped on-device log and a Diagnostics screen the user can share. It stays on the device: a shipped app that cannot see its own runtime failures is guessing, and the worstmaterial_uifailures never reachflutter analyze. -
Marionette — an agent can drive the running app: read the widget tree, tap, type, scroll, screenshot, hot reload.
MarionetteBindingis installed underkDebugModeonly, so a release build carries none of it, and the MCP server is declared in.mcp.json. It needsdart pub global activate marionette_mcponce per machine. -
Debug builds that install beside the store build. Debug and profile builds carry
<applicationId>.devand aDevlauncher label, soflutter runadds a second icon rather than replacing the copy installed from the store — testing a change never costs you the real app and its data. Release is untouched, and so is everything that names the store build: the Gradlenamespace,AppConfig.androidApplicationId, themarket://rate link and fastlane'sAPP_IDENTIFIER. Off with--no-dev-suffix. -
An accessibility baseline, tested. Every tappable is labelled and at least 48dp, on desktop too; text clears WCAG AA contrast in both themes for every accent; the device text size is honoured uncapped and every screen lays out at 200%; and on desktop, Tab finishes the navigation before it enters the content.
test/accessibility_test.dartrunsmeetsGuidelineon each shipped screen, and a new screen joins it with one line. -
Seams for analytics and feedback.
AnalyticsandUserFeedbackinterfaces with no-op defaults inlib/core/hooks/. Nothing leaves the device until you install an implementation with a provider override. -
Responsive helpers, shared UI (
AsyncView,EmptyView,ErrorView,SectionLabel,context.toast,context.confirm), and aLauncherfor every hand-off to another app. -
PROJECT.mdas the single guide — scope, workflow, conventions, the responsive contract, a testing strategy and a done-checklist.AGENTS.mdis canonical above it: it carries the shared working rails between<!-- popupbits:rails:begin -->/<!-- popupbits:rails:end -->markers, then points atPROJECT.md.CLAUDE.mdis exactly@AGENTS.md— the import form Claude Code follows, since it never readsAGENTS.mdon its own and a Markdown link is not an import. -
Agent skills in
.claude/skills/:material-ui(the frozen-vs-modern Material traps),store-readiness(will the submission be accepted),app-store-optimization(will the listing be found and installed), andmobile-ui-design. -
CI on day one —
.github/workflows/ci.ymlrunsflutter analyzeandflutter teston every push and pull request, so the generated guards actually hold. Releasing stays separate and manual. -
Store screenshots as a workflow — manual workflows capture on a matching emulator or simulator, render with moksha, and open a PR. One renderer covers Play phone and tablet plus iPhone and iPad, from one committed copy file, so the listings look like the same app. Every image is checked against the store's upload rules first — an alpha channel, Play's 2:1 aspect limit and Apple's accepted dimensions each reject a perfectly valid PNG, and all three now fail in CI instead of at upload.
-
A skill for beej itself, shipped the way Dart packages now distribute them, so an agent in any repo can learn to scaffold with it:
dart run skills@ add https://github.com/lohanidamodar/beej.git -
Agent tooling:
.mcp.jsondeclaring the Dart MCP server (it ships inside the Dart SDK, so nothing to install) plus Appwrite's hosted server when that backend is on, and project-scoped skills under.claude/skills/. Turn it all off with--no-agent-config, or narrow it with--skills=material-ui.
What you choose
| Option | Values | Default |
|---|---|---|
org |
reverse-DNS prefix | com.example |
locales |
en plus any of ne |
en |
platforms |
android, ios, web, windows, macos, linux | all |
backend |
appwrite, none |
none |
router |
go_router, navigator |
go_router |
nav |
tabs, drawer, tabs+drawer |
tabs+drawer |
database |
sqflite, none |
none |
icons |
picons, material |
picons |
features.devSuffix |
bool | true |
features.inAppUpdate |
bool | true |
features.notifications |
bool | false |
features.nepaliDates |
bool | false |
features.onboarding |
bool | false |
features.review |
bool | true |
tooling.fastlane |
bool | true |
tooling.githubWorkflow |
bool | true |
tooling.screenshots |
bool | true |
agents.mcp |
bool | true |
agents.skills |
all / none / list |
all |
beej bricks lists what each contributes.
Onboarding. features.onboarding: true adds a skippable welcome of
two to four pages (three placeholders to start), shown once on first
launch, before sign-in.
Where app data lives. database: sqflite stores its file through
appDataDirectory() in lib/core/storage/app_data_directory.dart, and
the onboarding seen-flag, and anything else the app keeps for itself — caches, settings files, a Hive box —
belongs there too. On Android and iOS that is the app-private documents
directory. On desktop it is the support directory (%APPDATA%\<org>\<name>
on Windows, from the runner's company and product names), never getApplicationDocumentsDirectory(), which on desktop is
the user's real Documents folder.
Making it yours
beej ships neutral defaults — com.example, English only, no About URLs — so
it is useful to anyone. beej config is where you set yours once instead of
passing the same flags to every create:
beej config # show current settings and their file
beej config keys # everything settable
beej config set org com.acme
beej config set locales en,ne
beej config set about.privacyPolicyUrl https://acme.com/{name-kebab}-privacy
beej config unset locales # back to the built-in default
The file is $XDG_CONFIG_HOME/beej/config.yaml (%APPDATA%\beej on Windows),
overridable with BEEJ_CONFIG. It is a spec file without a name, so it
accepts exactly the same keys as --spec and is hand-editable.
About URLs accept {name} and {name-kebab}, expanded per project — which is
what makes a per-app privacy-policy URL storable as a one-time preference.
Anything you do not set is simply absent: an About row is generated only when its value exists, because a row linking nowhere is worse than no row. beej warns when no privacy-policy URL is configured, since both stores require one before release.
Resolution order: built-in defaults < config < spec file < flags.
For agents
beej spec --schema # JSON Schema for the spec file
beej spec --example # annotated example
beej create --spec app.yaml --yes
Precedence is flag > spec file > prompt > default. --yes never prompts,
so it is safe in a script. --dry-run prints the resolved plan and writes
nothing.
How it works
flutter create produces the platform folders, then bricks render template
files over that skeleton. Bricks are additive — a feature that is off simply
never contributes. Three registries in the generated app are the seams a
feature extends: core/bootstrap.dart, core/router/routes.dart, and
features/settings/tiles.dart.
A brick can also patch a file flutter create wrote, for the few values
that cannot be owned as a whole file — project.pbxproj carries generated
UUIDs, and owning AndroidManifest.xml would mean re-shipping the activity
config on every Flutter release. Patches are anchored and asserted: if a
Flutter release changes one of those files, generation fails naming the file
and the anchor rather than quietly skipping the edit. --dry-run lists them
alongside the files.
Install
Requires Flutter 3.50 (Dart 3.14) or newer. Generated apps target the same
line: sdk: ^3.14.0, flutter: '>=3.50.0'.
dart pub global activate beej
One command, no clone. pub compiles a snapshot at activation time, so runs are
clean and quick (~70ms) and nothing is re-resolved per invocation. Make sure
~/.pub-cache/bin is on your PATH.
For an unreleased change, activate from git instead:
dart pub global activate --source git https://github.com/lohanidamodar/beej.git
If you are working on beej itself
./tool/install.sh # compiles a standalone binary to ~/.local/bin/beej
~3ms rather than ~70ms, and it re-embeds templates first so a stale one cannot ship. The binary does not track source edits, so re-run it after changing beej.
What not to use
dart pub global activate --source path . re-resolves dependencies on every
invocation and prints "Resolving dependencies..." to stdout, which corrupts
the output any agent or script is reading. It also starts in ~3s. Use one of
the two above instead.
Development
dart test # beej's own tests — seconds
dart run tool/verify_matrix.dart # generate 11 configs, analyze + test each
dart run tool/verify_matrix.dart minimal --keep
dart run tool/embed_templates.dart # after editing anything under templates/
Editing a template takes effect immediately when running from source —
TemplateSource prefers the directory and falls back to the embedded copy. A
test asserts the two agree, so a forgotten re-embed fails CI rather than
shipping stale templates in a binary.
The matrix is the real safety net. Mutually exclusive bricks can never coexist in one project, so "the templates compile" is not achievable — proving the output compiles is what keeps fifteen toggles from rotting.
Package versions live in one place: lib/src/bricks/versions.dart. Bump there,
then re-run the matrix.
What a generated project depends on
One external repository: popupbits/fastlane-plugin-play_publisher, which is
public. The release workflow is written into the project rather than calling a
shared reusable workflow, so nothing in a generated repo depends on private
infrastructure.
Known limitations
The screenshot workflows have never run on a runner. Everything upstream of
them is verified — capture, the job builder, and moksha rendering correct sizes
for every device family — but emulator boot, the moksha checkout and
gh pr create are unproven. Expect to fix something the first time.
No App Store preview video. moksha renders stills. Apple's 15-30 second recording is a separate job.
material_ui is an unusual choice. It is Material decoupled from the
Flutter SDK, and everything generated here imports it instead of
package:flutter/material.dart. If you want plain Flutter Material, beej is
the wrong starting point — that assumption runs through the templates, the
tests and the docs, and is not a flag you can flip.