Skip to main content

What does dartway create give you?

dartway create my_app

Four Dart packages side by side, an agent toolkit, and a git repository with an initial commit. The source is template/ in the DartWay monorepo — a skeleton with auth, roles, navigation, an admin panel and a UI kit, and zero domain models.

my_app/
my_app_server/ Serverpod backend — models, CRUD configs, business logic
my_app_client/ generated protocol + API client — never edited by hand
my_app_flutter/ the app — features, UI kit, navigation
my_app_shared/ rules both sides must apply identically — pure Dart, no dependencies
.claude/ the agent toolkit (installed, then committed)
.vscode/ Server / Flutter (web) launch configs
.github/ a Claude PR-review workflow (delete it to turn review off)

A new project gets no docs/ folder, deliberately. What a screen does belongs in its DwFeatureSpec, the server-side rules in doc comments above the CRUD config, a cross-cutting registry — analytics events, settings keys, roles — in code under lib/core/, where the compiler knows the list and a typo is an error. A document sitting apart from the code goes stale without anything failing: nothing compiles it, no checker sees it, and the next reader — increasingly an agent — believes it.

A project may still keep the documents it genuinely needs there, and one kind is known to the framework because it keeps being reinvented: docs/adr/ — the decisions and what they ruled out. An ADR exists for the one thing that cannot live beside code — the rejected alternatives, which have no file to sit next to. The rules for writing one (and for never editing it) are in the agent toolkit's CLAUDE.md; dartway-plan reads the folder before proposing an approach.

Why four packages

server and flutter cannot depend on each other — one imports dart:io and Serverpod, the other imports Flutter. client holds the generated protocol, and flutter depends on it. The server does not: it carries its own copy of the same models under lib/src/generated, which is why the protocol is generated on both sides rather than shared through one package — and why a shared package must not route through the client, see below.

The fourth, my_app_shared, is pure Dart for code that has to behave identically on both sides — format validation, shared enums, computation over fields with no IO. The skeleton ships it wired into both halves, holding one worked example; it used to be a package each project assembled by hand, and the guidance for doing so was wrong about the one thing that matters.

It depends on nothing, and that constraint is its design. It may not depend on the client package: the server does not depend on that package either — it carries its own copy of the generated models — so a shared package reaching for the protocol would serve exactly one of the two sides. Plain values in, plain values out; each side unpacks its own models at the call site.

my_app_server — where the rules live

lib/src/ here is a closed list too, for the same reason and enforced by the same check.

my_app_server/
bin/main.dart the server entry point
config/ development / staging / production / test + passwords.yaml
migrations/ generated schema migrations
lib/server.dart the package's public surface
lib/src/
models/ your *.spy.yaml model definitions
generated/ serverpod generate output — do not edit
crud/ one DwCrudConfig per model — the feature's whole behaviour
dartway/ DwCore.init and the session role helpers
domain/ pure rules over models — no Session, no IO, no DB
app/ session-aware workflows — bootstrap_admin.dart lives here
endpoints/ hand-written Serverpod endpoints, for the things CRUD is not — absent
until you need one
web/ server-rendered pages, if you want any
test/ DartWay's auth and password integration suites, against a real DB

domain/ and app/ are the one boundary worth holding on the server — pure rules against session-aware side effects. domain/ ships empty, because until such code exists the folder is nothing; app/ ships with the one workflow every project needs before it has a domain at all — bootstrap_admin.dart, which brings the identifier declared in bootstrapAdminIdentifier to the state "the profile exists and it is an admin" on every boot. The admin role is granted by an admin, so the first one is declared per environment; that is also the answer for staging and production, where the alternative is an UPDATE typed by hand that nobody can read back or repeat. Both folders are declared, so creating one is not an invention; anything else under lib/src/ is.

endpoints/ is not there at all, and that is the point rather than an omission: the skeleton reaches its whole surface — auth, profiles, roles, settings, an admin panel — through CRUD configs, without one hand-written endpoint, and serverpod generate is perfectly happy with no endpoints to generate. Create the folder for the things CRUD genuinely is not — uploads, webhooks, a third party's callback — and treat it as the last resort it is.

Models are YAML, not Dart. lib/src/models/note/note.spy.yaml declares a class, its table and its fields; serverpod generate turns it into Dart in lib/src/generated/ and in the client package. You edit the YAML; you never edit the generated Dart.

A config per model is the API. lib/src/crud/ holds one DwCrudConfig<T> per exposed model, and a model with no config in DwCore.init(crudConfigurations: [...]) is not reachable at all. The skeleton ships two of them — user_profile_crud_config.dart and app_setting_crud_config.dart — which are worth reading before writing your first: between them they show an admin-only access filter, a role guard on writes, validation, and a public realtime broadcast.

lib/src/dartway/ is the wiring: dartway_core.dart builds DwCore.init<UserProfile> with the CRUD list, the auth config and the verification-code sender; dartway_session_extension.dart holds the role helpers (session.isAdmin, adminOnlyAccessFilter) that the configs read. Both are yours to extend — a new role helper goes here, not into each config.

lib/src/endpoints/ still exists, and that is deliberate. CRUD covers data; a report, an import or a payment callback is an ordinary Serverpod endpoint you write yourself.

my_app_client — generated, and that is the point

serverpod generate, run in the server package, writes this package: the model classes, the Client the app connects with, and the serialisation. Nothing here is edited by hand — the next generate would erase it, silently and without breaking the build.

If you find yourself wanting to add a method here, the answer is elsewhere: a rule belongs in a CRUD config on the server, an extension over a model belongs in the Flutter package.

my_app_flutter — where the app is

The top level of lib/ is a closed list: two files, four zones, four layers. Nothing else may sit there, and each of these means one thing and is spelled one way.

my_app_flutter/lib/
main.dart development parameters only (backend URL, version label)
my_app_app.dart all the wiring: DwAppRunner, MaterialApp.router, the root subscription

ZONES — features, each with a DwFeatureSpec
app/ the app itself — app/home/, app/profile/, ...
admin/ the admin panel
auth/ the sign-in flow
common/ features more than one zone draws on (create it when that happens)

LAYERS — everything that is not a feature
core/ app-wide wiring: router/, dw_core.dart, app_settings/, studio/, platform/, dev/
shared/ building blocks: widgets and helpers with no story of their own
ui_kit/ your design system, as source
l10n/ ARB files and their generated output

main.dart holds nothing but the concrete environment (the backend URL differs on an Android device, which is the one line most people edit first). Everything structural is in the app file.

web/index.html is part of the app too. It sits outside lib/, which makes it easy to read as scaffolding, and it is not: the shell the skeleton ships carries a scroll lock, and without that block focusing a text field on iOS scrolls the document — and with it the Flutter canvas, which is exactly one layout viewport tall — off the screen. Nothing fails and nothing is logged, and neither the iOS simulator nor a desktop browser reproduces it; a real phone does, which is where a staging build gets opened. Anything that regenerates the shell drops the block silently, so grep -q 'focusin' web/index.html is the check worth running before you hand a web build to someone.

A zone is not a feature and not a folder you invent. The four are the kinds of thing an app is made of, not a list of sections: a fifth navigation zone in the router does not earn a folder of its own — it is a group inside app/, like any other group. The zones are also the only places asked for a DwFeatureSpec, which is why the admin panel has to be one and cannot live at app/admin/: nested in a zone it reads as an ordinary group, and the whole panel disappears from every question the checker asks about zones.

core/studio/ is the DartWay Studio bridge binding — the host that talks to Studio when the app runs inside its preview frame, plus the screen passports Studio renders beside it. It is wiring like the router, inert outside an iframe, and a project that never opens Studio can delete the folder.

core/platform/ is where a conditional-import trio lives — x.dart holding export 'x_stub.dart' if (dart.library.js_interop) 'x_web.dart';, plus the two halves. It is one symbol whose implementation the platform picks, which is wiring by any reading; and a feature that keeps its own copy has quietly acquired a second answer to the same question. dartway check names this folder in the unusedFeatureFile finding, so the shape does not have to be guessed at.

There is no data/ and no domain/. The data layer is dw.repo, so a data/ folder in a DartWay app is either empty or a second way to do the same thing; and the rules of a DartWay app live in CRUD configs on the server, so what is left on the Flutter side — extensions on models, formatting, predicates — is a helper, and helpers live in shared/. Both folders were conventional once, both stayed empty in every skeleton, and a name that exists only in a document is how a layout drifts.

The conventions checker enforces this list rather than describing it: an undeclared folder, a stray file at the root of lib/, a missing my_app_app.dart and a top-level name nested inside a zone are all errors.

A feature is a folder with one public file

lib/app/bookings/
bookings_page.dart the entry point — the feature's whole public surface
widgets/ its own widgets
logic/ its own providers, filters, mappers

The shape is inferred, not declared: a folder with a root .dart file is a feature, a folder without one is just a group that nests features. Only widgets/ and logic/ count as a feature's internals — any other subfolder is read as a nested feature.

Two rules follow, and the conventions checker enforces both: a feature has exactly one root file, and no feature may import another feature's widgets/ or logic/. Behaviour two features share is one more feature; a widget with no story of its own is a building block and lives in lib/shared/, where no spec is expected of it. The entry point also declares what it is, in a DwFeatureSpec next to its own code rather than in a document that drifts — see features and specs.

A feature lives in a zone, and only there. The zone it belongs to is the one that owns the behaviour, not the one that happens to show it first: a screen the admin panel and the app both open belongs in common/, and a widget both draw with is not a feature at all — it is a block in shared/.

Why the kit is source in your app, not a dependency

lib/ui_kit/ is a real design system — AppText, AppButton, AppCard, a theme, formatters — and it is yours, copied in, not imported. The framework ships no design on purpose: a design system is the one thing every serious app ends up owning, and shipping it as a dependency only starts an argument about the corner radius of a button. Change any of it without asking anyone, and without waiting for a release.

Inside, files are grouped by how often you reach for them — 1_essentials/, 2_frequent/, 3_special/ — plus theme/, layout/, utils/ and assets/. The kit is one library: every file is a part of '../ui_kit.dart', and the rest of the app imports the ui_kit.dart barrel and nothing deeper.

The boundary is enforced rather than remembered: raw Color(...), TextStyle(...), BorderRadius and direct Theme.of(context) access outside ui_kit/ are a lint (dartway_lints, wired into custom_lint). A style that leaks into a feature is a style nobody can change centrally later.

The same lint package draws one more line, this time about imports: a relative import may walk at most two levels up (deep_relative_import). One or two ../ read as "the feature next door"; past that the path names nothing, and the destination — core/, shared/, ui_kit/, another zone — is spelled out with a package: import instead. The limit doubles as a structure signal: a sibling four levels away is not a sibling.

.claude/ — generated, and committed

The agent toolkit is installed into .claude/ by create (and refreshed by dartway setup-ai): the project's CLAUDE.md, the dartway-* skills, and a couple of commands. It is a generated-but-committed artifact, like the Serverpod protocol — regenerate it to pick up framework changes, but commit the result so the repository is self-contained. See the agent toolkit.

What create changes on the way in

Copying is not all it does. Every occurrence of dartway_starter becomes your project name and DartwayStarter becomes its PascalCase form, in file names and in file contents alike. Each package's dependency_overrides block — which points at the monorepo checkout and only makes sense inside it — is stripped, so the framework dependencies resolve from pub.dev like any other. The toolkit lands in .claude/, with a default settings.json if the project has none, and docs/dev_notes/ — the project's own findings, one tracked file per finding — is created beside it.

Worth knowing if you ever put such a block back — inside the monorepo, or in your own project while debugging a framework package. An override does more than redirect one dependency: for the package it names, pub stops checking constraints altogether. Not "prefers the override" — the caret is never evaluated, so a line that no published version could satisfy resolves in silence for as long as the block is there. That is the same property plugins warns against using as a way to consume the framework, seen from its other side: the override is not only global to the resolution, it also hides whether the constraints underneath it still say anything true. The first tree to find out is one without the block — a new project, or CI.

So after adding a framework package to a project, resolve it once for real: run dart pub upgrade <the dartway packages> in each package that gained one, and check that they came out on a single commit. dartway check reports it when they did not — see the conventions checker.

Where to go next