What does dartway create give you?
dartway create my_app
Three Dart packages side by side, the files that deploy and review them, an agent toolkit, and a git
repository with an initial commit. The source is template/ in the DartWay monorepo — a skeleton
with sign-in, profiles, roles, an admin panel and a UI kit, and no domain models.
my_app/
my_app_shared/ the contract: data objects, requests, commands, channels, refusal codes
my_app_server/ row classes, handlers, access and channel rules, migrations
my_app_flutter/ the app: features, navigation, the UI kit
deploy/ the deployment's configuration — dart run dartway_cli:dartway deploy reads it
.claude/ the agent toolkit, installed by create and committed
docs/dev_notes/ the project's own findings, one file per finding
CLAUDE.md the project's own rules for agents — yours, never overwritten
README.md bring-up, a feature end to end, the checks
.vscode/ Server and Flutter launch configurations
.github/ CI running every check as one check, ci; an optional Claude review
Why three packages
my_app_server imports dart:io and Postgres; my_app_flutter imports Flutter. Neither can depend
on the other. What they must agree on — every call, every object, every refusal code — lives in
my_app_shared, pure Dart, and both depend on it. There is no generated client package: the
shared package is the client contract, compiled into the app and into the server from one source.
That is also what makes a shared rule honest. AuthIdentifier.normalize in the skeleton's shared
package is the function the app runs on what a person typed and the server's
DwAuthConfig.normalize. Written twice, the copies drift — and since signing in with an unknown
identifier creates an account, a phone stored as +7 999… on one side and 7999… on the other is a
second, empty account instead of a sign-in.
my_app_shared — the contract
my_app_shared/
lib/my_app_shared.dart the library: re-exports dartway_core_shared and everything below
lib/generated/dw_protocol.dart the protocol registry — generated
lib/src/
profile.dart, admin.dart, one file per feature of the server, named after its folder:
settings.dart, account.dart data objects, requests and commands, and the rules both sides
apply (account.dart: the identifier's one form, the sign-up
keys); each with its generated *.dw.dart part when it
declares data objects
my_app_channel.dart enum MyAppChannel with DwChannelKind — the live channels
my_app_refusal.dart enum MyAppRefusal with DwRefusalCodes — why the server says no
my_app_upload.dart enum MyAppUpload with DwUploadPurpose — what a file is for
my_app_protocol.dart the protocol both sides speak: the generated one and the modules'
test/my_app_shared_test.dart
lib/src/ mirrors the server's features — a law, held by dart run dartway_cli:dartway check
(invalidSharedLayout). The contract of the server's profile/ is src/profile.dart; when it
grows, it becomes a flat folder src/profile/ of parts only, profile_<part>.dart, each a group of
DTOs — a data object with the requests and commands that answer it (chat_messages.dart), never
exactly a layer (chat_models.dart) — and no profile/profile.dart beside them, where everything
without a part would collect. A name is a feature folder of the server, letter for letter, so "where
is the contract of this feature" has one answer, and a DTO goes where its handler is. The shared
package has no core/: what exists only in the contract — a rule both sides apply, the keys a
sign-up sends — goes with the feature that owns it. Beside the features sit only the package-wide
files named after the package: _channel, _refusal, _upload, _protocol and, with push,
_push_category. lib/ itself holds the library, which re-exports and declares nothing,
generated/, which holds only what the generator writes, and src/.
It depends on dartway_core_shared and nothing else. Whatever it declares is compiled into the
server and into the app alike, so it cannot reach for Flutter, a database or IO. A rule that needs the
database is not a shared rule — it is a handler's.
The three enums are named after the shared package — my_app_shared declares MyAppChannel,
MyAppRefusal, MyAppUpload, the prefix taken mechanically from the package's name — so a project's
own codes never read as the framework's, and no two projects spell the rule differently.
my_app_server — where the rules live
my_app_server/
bin/server.dart starts the server — configured by the environment alone
bin/migrate.dart apply | rollback | status | create <name> | check | rehash
bin/seed_dev.dart development accounts and data; refuses to run twice
lib/my_app_server.dart builds the DwAppServer: protocol, schema, migrations, auth,
the features, every upload purpose's rule — the one file
that sees every feature
lib/generated/
dw_schema.dart the schema and the db.<table> getters — generated
lib/src/
core/ what every feature imports, and which imports no feature:
fixed names, App* classes
channels.dart the channels handlers publish to
environment.dart the configuration, read once at start
files.dart the storage's default bucket names
migrations/ migrations.dart and one file per migration — written by
migrate.dart create, then yours
profile/ a feature: everything of one area of the app, in one folder
profile_feature.dart the DwServerFeature: its handlers, channel rules, jobs, routes
profile_rows.dart row classes (@DwSqlTable) with their generated *.dw.dart
profile_handlers.dart one DwCallHandler per request and command
profile_objects.dart rows → the data objects clients see, related data in batches
profile_publications.dart what a change publishes, and to whom
profile_jobs.dart its job kinds and definitions, when it has jobs
profile_access.dart its access rules, when they outgrow the handlers
profile_routes.dart its DwHttpRoute doors, when it has them
profile_changes.dart how another feature writes its rows — the one place their
invariants are kept, a change published through its own
publications
logic/ everything else the feature needs, flat, when it needs it
profile/ in the skeleton, the sink every feature imports: its
profile_access.dart is what "the caller" means to this app
(ctx.profile, the role rules, the avatar's upload rule), its
profile_changes.dart every write of a profile from outside
account/ the feature at the top: the sign-in hooks (logic/auth.dart —
code delivery, the profile made with each account) and the
first administrator, made at every start
admin/, settings/ the skeleton's other features, the same shape
test/ acceptance tests on a real server, database and storage
docker-compose.yaml development Postgres and RustFS
Dockerfile the server image the deploy builds
lib/src/ is folders: core/, migrations/, and one per feature — a law, held by dartway check as the Flutter package's top level is. A feature's folder holds everything of its area — its
rows, handlers, objects, publications and jobs, each in a file of its kind (below) — and
declares itself in <feature>_feature.dart as a DwServerFeature the server
lists. No file sits at the top of src/, and no folder anywhere under it is named for a layer (the
list is below): a feature split across layers lives in
four places, and a project that grew that way ended with a chat/ beside a domain/chat/ and two
rules for who is in a chat. src/migrations/migrations.dart is a fixed name — bin/migrate.dart
writes and reads migrations by that path.
Inside a feature the file set is closed, so that one question has one answer in every
project. A feature folder <feature>/ holds:
- files named
<feature>_<kind>.dart, the kind one offeature,rows,handlers,objects,publications,jobs,access,routes,changes— exactly one<feature>_feature.dart, the rest when the feature has them; <feature>_<part>_<kind>.dartwhen a kind outgrows one file —orders_refunds_handlers.dartbesideorders_handlers.dart. This is the only way a feature splits;- one optional subfolder,
logic/, for everything that is none of the kinds: a client of an outside service, a calculator, the rules of a domain. Names inside it are free, but none ends in a kind (logic/send_handlers.dartis a handlers file in hiding), and it is flat: logic that needs folders of its own is a feature too big for one folder, and it splits into features, or into<part>files.
No other subfolder, and no folder anywhere under src/ — core/ included — named for
a layer: domain, rows, handlers, services, models, objects, repositories, utils,
helpers, entities, publications (invalidServerFeatureFile).
And each kind is held by what the file declares, not by its name alone
(misplacedServerCode):
| Declared | Only in |
|---|---|
a handler — DwCallHandler.…(, a <DwCallHandler>[…] list | *_handlers.dart |
a row class — @DwSqlTable, extends DwTableRow | *_rows.dart |
a job — DwQueuedJob, DwRecurringJob, a DwJobKind constructed | *_jobs.dart |
a route — DwHttpRoute.…(, a <DwHttpRoute>[…] list | *_routes.dart |
DwServerFeature( | <feature>_feature.dart |
a publication — a named function, method or closure-holding field that calls ctx.publish(, in closures it runs too (forEach, transaction) — but not in a hook it hands to a constructor by name (DwAuthConfig(onAccountCreated: …)) | *_publications.dart |
a mapping — a named function, method or closure-holding field that takes a row (a …Row parameter, or a member of a row class or of an extension on one), returns a data object of the shared package, and builds one | *_objects.dart |
core/ holds none of them. What is not a declaration is not judged: a handler or a sign-in hook
publishing inline (ctx.publish inside its closure) is a call, not a publication, and a mapping
written as a closure inside a handler is not seen — keep it in _objects anyway, since that is
where the next reader looks for it.
A file is held to the app's length, in the server and the shared package alike: over 200 lines
is a nudge (fileLong), over 350 a warning (fileTooLong). Passed over: generated code (named so
and carrying the generator's header), migrations, and the server's seed data — a file of nothing
but consts, each a row draft or a collection of drafts for a DwSeedRows step, so a catalogue of
hundreds of rows sits in a <feature>_<part>_rows.dart of its own. Tests are not
measured; a long one splits by scenario (<feature>_<scenario>_acceptance_test.dart).
Features import each other one way, through a surface, and write only their own rows — four
errors of dart run dartway_cli:dartway check,
so that "who defines this" has one answer:
core/imports no feature (coreImportsFeature). Every feature importscore/; a featurecore/imported would be imported by every feature, itself included. So who the caller is —ctx.profileand the role rules — is the profile feature's (profile/profile_access.dart), each upload purpose's rule is in the_access.dartof the feature it belongs to, and the sign-in hooks areaccount/, the one feature above those an account's life touches.lib/my_app_server.dartassembles them and is not judged; nothing undersrc/imports it.- No cycles (
featureImportCycle), counting every import. Two features that import each other are one feature in two folders: neither can be read, tested or removed alone, and a rule each needs from the other ends up written in both — a project that grew that way had chat membership defined in four places. The profile is the graph's sink: every feature asks who the caller is, so the profile imports no other feature. - A feature imports another only through its surface (
featureImportOutsideSurface): its_rows(to read and join its tables), its_access(its rules — whose row, who is a member, one function per concept), its_objects(its rows as it shows them), its_publications(its changes as it announces them) and its_changes(its rows written the one way that keeps their invariants), and their<part>files. Never its_feature,_handlers,_jobs,_routesorlogic/: those run the feature, and another feature running them is where a concept gets a second definition. - A row is written only by the feature that owns its invariants (
foreignRowWrite). Its_rowsare read by anyone who imports them, butctx.db.<table>.insert,update,delete,upsertof another feature's table fails, from a feature or fromcore/: the writer calls the owner's_changesinstead. The skeleton's admin panel changes a role throughProfileChanges.changeRole, and its sign-in hook creates the profile throughProfileChanges.create: one definition of a valid profile.
A row is not a data object. UserProfileRow is a table; UserProfile is what a client receives.
The server builds one from the other in profile_objects.dart — the profile's phone and e-mail come from
the framework's identities, its photo URL from the file store — so a column added for the server's
own use never reaches a client by accident.
Accounts are the framework's, profiles are yours. The framework keeps accounts, sign-in identifiers
and session keys in its own tables; UserProfileRow references the account and is created in the
same transaction, by onAccountCreated in account/logic/auth.dart. A signed-in account without a profile cannot
exist, and nothing about who a person is to your app lives in the framework.
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 name means one thing.
my_app_flutter/lib/
main.dart development parameters only: the server address, the version
my_app_app.dart the wiring: the core, DwAppRunner, MaterialApp.router
ZONES — features
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: dw_core.dart, router/ (with the zones' shells),
refusal_text.dart, update_required_page.dart, profile/, dev/
shared/ non-visual helpers several features use: extensions, formatters
ui_kit/ your design system, as source — styles and every visual building block
l10n/ ARB files and their generated output
core/dw_core.dart declares the ambient core — late DwFlutterCore dw; — and the function that builds
it. It is not final because a widget test builds its own core against an in-memory server
(DwFakeServer) and disposes it in tearDown. core/refusal_text.dart is the catalogue that turns
every refusal code into the user's language; an exhaustive switch, so a code added to the shared enum
does not compile here until it has a text.
There is no data/ and no domain/. The data layer is dw.request and dw.command over the
shared contract, so a data/ folder is either empty or a second way to do the same thing. The rules
live in the shared package, where both sides apply them, and in the server's handlers; what is left on
the Flutter side — extensions on data objects, formatting, predicates — is a helper, and helpers live
in shared/.
A zone is 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 does not earn a top-level folder — it is a group inside app/. The
admin panel cannot live at app/admin/ either: a top-level name nested in a zone reads as an ordinary
group, and the checker reports it.
The conventions checker enforces the 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 inside a zone are all errors.
A feature is a folder with one public file
lib/app/profile/profile_page/
profile_page.dart the entry point — the feature's whole public surface
widgets/ its own widgets
logic/ its own providers and helpers
The shape is inferred, not declared: a folder with a root .dart file is a feature, a folder
without one is a group that nests features. Only widgets/ and logic/ count as a feature's
internals; any other subfolder is read as a nested feature.
A feature has exactly one root file, and no feature imports 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 in lib/ui_kit/. The entry point — and no other file of the feature — declares what it is in a
DwFeatureSpec beside its code, and a feature sends its commands from its logic/ — see features
and specs.
Why the kit is source in your app, not a dependency
lib/ui_kit/ is a 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 one as a dependency starts an argument about
the corner radius of a button. Change any of it 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 that file and nothing deeper.
The boundary is enforced by dartway_lints, an analyzer plugin: raw Color(...), TextStyle(...),
BorderRadius and direct theme access outside ui_kit/ are flagged, because a style that leaks
into a feature is a style nobody can change centrally. Spacing is the kit's too: a gap or an inset
outside it is an AppSpace token. See the UI kit.
Imports in lib/ are package: only, in all three packages — dart run dartway_cli:dartway check
fails a relative one (relativeImport) and --fix rewrites it. Under test/, the harness in
test/support/ is imported relatively, since package: cannot reach it.
web/index.html is part of the app
It sits outside lib/, which makes it easy to read as scaffolding, and it is not: the skeleton's shell
carries a scroll lock. Without it, focusing a text field on iOS scrolls the document — and with it the
Flutter canvas — off the screen. Nothing fails and nothing is logged, and neither the iOS simulator
nor a desktop browser reproduces it; a real phone does. Anything that regenerates the shell drops the
block silently, so grep -q 'focusin' web/index.html is worth running before you hand a web build to
someone.
Generated files, and who writes them
| File | Written by | From |
|---|---|---|
*_shared/lib/src/**/*.dw.dart | dart run dartway_cli:dartway generate | data objects, requests, commands: codecs and equality |
*_shared/lib/generated/dw_protocol.dart | dart run dartway_cli:dartway generate | every DTO of the contract: the protocol registry |
*_server/lib/src/<feature>/*_rows.dw.dart | dart run dartway_cli:dartway generate | row classes: the typed table definitions |
*_server/lib/generated/dw_schema.dart | dart run dartway_cli:dartway generate | row classes: the schema and the db.<table> getters |
*_server/lib/src/migrations/m<timestamp>_<name>.dart | dart run bin/migrate.dart create <name> | the difference between the schema and the migrations — a draft you review, then yours |
*_flutter/lib/l10n/gen/ | Flutter's gen-l10n | the ARB files |
Generated files are never edited by hand, and they are committed. The next generation would erase
an edit silently, and dart run dartway_cli:dartway generate --check — part of dart run dartway_cli:dartway check — fails on any generated
file that no longer matches its sources, or whose source is gone. The codecs are the wire: a field a
stale part does not know compiles, starts, and travels without that field.
A migration is the exception: drafted once, then owned. create seals it with a checksum of its
source; an edit made while reviewing an unapplied draft is re-sealed with migrate.dart rehash, and
migrate.dart check fails on a file that no longer matches its seal — an applied migration edited in
place is a database that no longer agrees with its history.
deploy/
The deployment is configuration, not scripts: deploy/config.yaml describes every environment of
the project — each deployment's machine, api and app hosts, optional site and storage, and
local, which is what your own machine starts a server with — plus requires, what the project
needs wherever it runs. dart run dartway_cli:dartway deploy setup, check and run do the rest; deploy/secrets.yaml
is the git-ignored half of the same file, and dartway secret maintains it. The server's Dockerfile
and the Flutter package's Dockerfile and nginx.conf are the images it builds. deploy/README.md
explains the stack; deploy explains the command.
.claude/, CLAUDE.md and docs/dev_notes/
Three kinds of file, and the difference is who may change them:
.claude/is installed bycreateand refreshed bydartway update(ordartway setup-ai): the toolkit's constitution, thedartway-*skills, the/commitand/dartway-checkupcommands, andsettings.json. What is managed versus yours to keep, and how it all works: the agent toolkit.CLAUDE.mdat the root is the project's own rules for agents: a DartWay default the project replaces, conventions the framework says nothing about, with the reason beside each.createwrites it once, nearly empty;setup-aiandupdatenever touch it.docs/dev_notes/holds the project's findings, one tracked file per finding. The toolkit seeds its form; the notes are the project's.
A new project gets no other docs/, deliberately. What a screen does belongs in its DwFeatureSpec,
a server rule in the doc comment of the handler that holds it, a cross-cutting list — settings keys,
roles — in code, where the compiler knows the list and a typo is an error. A document apart from the
code goes stale without anything failing.
What create changes on the way in
Copying is not all it does. Every occurrence of dartway_starter becomes your project name, in file
names and contents, with its PascalCase, camelCase and dashed forms (the dashed one names the storage
buckets). The renamed code is formatted once, so the first commit is already the formatter's. Each
package's monorepo dependency_overrides block is replaced by git overrides for exactly the
framework packages it reaches, pinned to the full template commit. With --framework-path, path
overrides onto that checkout are written instead.
Worth knowing if you ever add such a block yourself: for the package an override names, pub stops checking constraints altogether. Keep the carets equal to the corresponding framework versions; they remain package metadata even though the immutable override supplies the actual source.
Where to go next
- Quick start — bring it up, sign in, run the checks.
- Data objects and generation — what goes in the shared package and what generation produces.
- Handlers and context — one handler per request and command, and the caller's context.
- Database and migrations — rows, queries and the migration CLI.
- Flutter core — what
core/dw_core.dartbuilds. - The conventions checker — the rules above, as a command.