crabster

V2 Scope

{% raw %} This document defines the scope, decisions, and roadmap of Crabster V2. It takes over from the V1 roadmap, which covers phases 0 through 10 (API-only V1) plus the ongoing documentation phase.

Status, as of 17 September 2026: nine phases of ten delivered. What is left is phase 18, whose definition of done asks for a blueprint maintained by somebody outside this repository — it is waiting on people, not on code.

This document was written in planning, and the condition it set then — "V2 only starts once V1 has shipped and been used in real conditions" — could never be met: nothing is published, there is nobody to hear from. ADR-0003 is where that was faced, by refusing to let an unsatisfiable condition block two phases.

1. Positioning V1 → V2🔗

V1V2
Promise"From a domain model to a production Rust API in 5 minutes""A Crabster project lives, evolves, and ships over time"
GenerationOne-shot + simple record additionIncremental regeneration, version upgrade, assisted merging
Application scopeAPI-only monolith, SQL, JWTMulti-service, NoSQL, OAuth2/OIDC, frontend (decision settled)
EcosystemBlueprint mechanism shipped, ecosystem emptyBlueprint ecosystem bootstrapped, official blueprints maintained
MigrationNew projects onlyReverse engineering of an existing database

V1 answers "how do I start". V2 answers "how do I keep going" — the shift from a skeleton generator to a lifecycle tool, which is what decides whether it stays useful past the first day.

2. V2's central objective🔗

If V2 were to deliver only one thing, it would be incremental updating of a generated project (Phase 11). It is the hardest problem in this space, the one V1 deliberately sidesteps, and the one that determines whether Crabster stays useful past day one or becomes a throwaway bootstrapping tool.

Every other V2 phase is subordinate to that: in any resource trade-off, Phase 11 comes before everything else.

3. V2 themes🔗

ThemePhasesMotivation
T1 — Generated code lifecycle11Make a Crabster project maintainable over time
T2 — Enterprise-grade security12a, 12bRemove the OAuth2/OIDC blocker for enterprise adoption
T3 — Distributed architectures13Multi-service generation
T4 — Extended persistence14Move beyond SQL only (NoSQL, search)
T5 — Migration & interoperability15Take over existing databases
T6 — Frontend16Resolve ADR-0001, deferred since V1
T7 — Deployment & ecosystem17, 18Kubernetes, cloud, community blueprint maturity
T8 — Creation experience19Ask for what a project needs rather than be told it in flags

4. V2 architecture decisions🔗

ADR-0002 — Incremental merging via protected zones + 3-way merge🔗

ADR-0003 — Frontend: generated typed client, then UI blueprints🔗

Why the provisional status is dropped, which is the actual decision here. The ADR was written as proposed, provisional, to be reconfirmed against real V1 usage feedback before implementation. That condition cannot be met: 0.1.0 is tagged and unpublished, the repository is private, and there are no users to hear from. Feedback cannot arrive before a release, and the release is not waiting on this — while the condition is. It blocks Phase 16, and through it the session half of Phase 12b, which needs somewhere to put a browser session. A condition that cannot be satisfied and that blocks two phases is not caution; it is a stall with a reason written on it.

So the decision is taken now and revisitable on evidence, rather than deferred until evidence that the deferral itself prevents. What would reverse it: first users saying the typed client is not enough and that one opinionated UI they did not choose beats none. That is an argument about adoption, and it can only be made after shipping.

Amendment of 2026-09-17 — the three official UI blueprints ship in the binary. This does not reverse the decision, and it is worth saying exactly why. What this ADR protects is that the core's own templates carry no UI framework and that the interface is replaceable; both hold, and the second is held by a test — a --blueprint of your own carrying a ui-react module replaces the shipped one wholesale.

What changed is not the decision but the reach. All three lived under examples/, which does not enter the published .crate: ui react was therefore a setting no installed binary could answer, while the README promised three interfaces. The first attempt from an installed binary answered "ui is not a setting any installed module answers to". A promise nothing keeps is the defect that was fixed, not the decision.

They are installed by default because they only ever add a module, claimed by a setting nothing else answers to: a project that does not write ui … is byte for byte the project it was before they existed. Blueprints that replace a built-in module by name — Diesel for record, GitLab for core — cannot be, stay under examples/blueprints/ and are asked for with --blueprint. That rule is held by a test rather than by this paragraph.

ADR-0004 — Non-SQL persistence outside SeaORM🔗

ADR-0005 — Microservices without a Spring Cloud equivalent🔗

ADR-0006 — Interactive creation, as a façade over the flags🔗

5. V2 roadmap🔗

Numbering continues from the V1 roadmap (phases 0-10). Phases 12 through 15 are largely parallelizable across contributors; Phase 11 is a structural prerequisite for all of them, because it changes how templates are written.

Phase 19 is ordered like Phase C was in V1 — not after the others but alongside them. It can start immediately with the questions that already have answers, and by ADR-0006 every phase below adds its own question when it lands. It is last in the table because it is the one that finishes last, not the one that starts last.

PhaseTitleDepends onPriority
11Lifecycle: upgrade & incremental mergingFull V1Critical
12aExtended authentication: OAuth2/OIDC resource server11High
12bDeclarative authorization, and the browser session12a, 16Medium
13Microservices11, 12aHigh
14Extended persistence (NoSQL, search)11Medium
15Reverse engineering an existing schemaV1 (Phase 3)Medium
16Frontend: typed client & UI blueprintsV1 (Phase 6)Medium
17Kubernetes & cloud deployment13Medium
18Blueprint ecosystem maturity11Ongoing
19Interactive project creation—Ongoing

Phase 11 — Lifecycle: upgrade & incremental merging🔗

Objective: a project generated with Crabster x.y can be updated to x.z and have its CDL model evolve, without losing manual modifications.

Delivered, ahead of the phase. Protected zones; crabster upgrade — regeneration from .crabster/, a file-by-file decision from the stamps; the generated-code snapshot in .crabster/snapshot/ and the three-way merge it makes possible; frozen migrations (write_once); and crabster apply, which turns a model that changed into new migrations. A @length bound now moves the column with it, on the two databases that enforce a width. What remains is An optional reference now reaches a table that exists, as a column and a foreign key, and a type that only grows is restated rather than refused. The regression corpus is in place, on both axes: crabster-cli/tests/corpus.rs carries a realistically modified shop across a templates version and names the fate of every file, and every_change_a_database_with_rows_can_be_told applies each shape of change in sequence to a SQLite database holding a row. And SQLite is told the only way SQLite can be told: a change to a table it has already written becomes a rebuild of that table, which is what closes the last of the four. Phase 11 is complete. What is still refused is refused for a reason that is not a database's limitation — a type change that is not one of the three lossless widenings, a mandatory reference added to rows that point at nothing, and a @unique taken away on the two databases that named the constraint themselves.

Tasks

Definition of Done


Phase 12a — OAuth2/OIDC resource server🔗

Objective: OAuth2/OIDC, a frequent precondition for enterprise adoption.

Why this is split. The phase used to ask for the Authorization Code + PKCE flow, which is a client flow: a browser redirect, a callback, a session. A generated project is an API — ADR-0001 — and an API is a resource server: it validates the token it is handed, it does not run a login. The client flow only makes sense once the backend also serves the browser, which is Phase 16's ground. So the resource server is 12a and stands alone; everything that needs a session moved to 12b.

The setting is auth oauth2, not authenticationType oauth2 as this document said before the service block existed. No parser change is needed for it: settings reach the engine as opaque pairs, and a module claims one in its manifest with [activation].

Delivered, except for the question. The auth-oauth2 module, discovery and a cached key set, validation against a pinned algorithm, the Authenticated extractor, configurable claim extraction, a Keycloak in the generated docker-compose with its realm imported, an integration test that asks that Keycloak for a token, and a CI job that runs the whole of it. What remains is the wizard question, which waits on Phase 19, and the role hierarchy — Keycloak's composite roles cover it at the provider, and whether it is worth declaring in the project is not yet answered by anything anyone has run.

One thing was fixed along the way that belonged to no phase: Authenticated documented that it could be taken in a handler the model generated, and could not. AppState takes fields from a module that needs them now.

Tasks

Definition of Done


Phase 12b — Declarative authorization, and the browser session🔗

Objective: move authorization out of hand-written handlers and into the model, and cover the flows that need a session.

Why it is not 12a. Both halves need something 12a does not. Declaring authorization in the model is the first language change of V2 — grammar, IR, validation, templates, and the reference in two languages — and it is orthogonal to which authentication module is running. The session half needs the frontend question settled, which is Phase 16.

The authorization half is delivered. @roles, @reads and @writes on a record, roles { … } declaring what may be named, and a role nothing declares refused with the line it was written on. The guard is generated as a parameter on the handler rather than a line inside it: one that does not take it does not compile against the route that needs it. Verified by running a generated project — no token is 401, a token carrying a role the record does not name is 403, a MANAGER reads what @reads(MANAGER, ADMIN) allows and is refused what @writes(ADMIN) does not, and ADMIN writes both. The same model generates the same guard under auth jwt and auth oauth2, which is checked rather than assumed.

Three things came with it. A guarded model in a project with no auth setting is refused before anything is written, rather than generating a project that does not compile. Both authentication modules now expose one surface, so the record module is written against Authenticated, Claims and AuthError without knowing which is running. And auth oauth2 can be tested offline at all: a resource server holds no signing key, so until now a generated test had no way to obtain a token its own service would accept — which would have left every guarded route untestable under that module.

And the session half is in, which closes the phase. session cookie on a service with auth oauth2 makes it a backend-for-frontend: /auth/login redirects to the provider with Authorization Code and PKCE, /auth/callback exchanges the code, and the browser gets a cookie while the token stays here. Verified against a real Keycloak, and held to it by a test that drives the whole flow: the redirect carries a challenge, the credentials come back as a code, and a request carrying nothing but the cookie passes a guard the model declared — then logging out makes the same request 401 again.

It did not need a UI blueprint after all, and saying why matters: the session was never the front end's to hold. It lives in a table here, the cookie carries an identifier, and what reaches the rest of the project is the header it already read. Any front end at all sits in front of that, generated or hand-written — which is what made this reachable once ADR-0003 was settled rather than once a blueprint existed.

Tasks

Definition of Done


Phase 13 — Microservices🔗

Objective: generate and run several Crabster services together (takes over and refines the former Phase 11 of the V1 roadmap).

Delivered. Its shape first: a model with several service blocks generates one project per service, @service distributes the records, and a reference that would cross a service is refused by name. Each service records a model of itself, so a service generated among others is the same project as that service generated alone, byte for byte — which is what keeps record, apply and upgrade working on one of them unchanged.

A distributed generation now writes a root above the services too: one docker-compose that builds and starts all of them, each on its own port with its own database. Verified by running it — two services up, both serving, both healthy.

The gateway is generated too: kind gateway on a service makes a reverse proxy in front of the others, routing on the segments the records own, with its table recorded so an upgrade reproduces it. Verified by running the whole system — a record created and listed back through the gateway, a second service reached through the same one, and a path nobody claims answered with a 404 from the gateway itself.

Consul is in: a service that says discovery consul registers itself and keeps itself registered, and a gateway asks the catalogue where a service is rather than trusting what was generated. Verified by running the system and then breaking it — with the generated address for orders pointed at a host that does not exist, the route still answered, because the address came from the catalogue.

Trace correlation works and is now held to it: the gateway forwards the request id, so one value follows a call across the system — verified by sending one and finding it in both spans. What that is not is a trace, and the distinction is written where a reader meets it.

Consul KV is in as well: a service reads settings from the store, under the files it ships with and over nothing. The gateway also limits what one caller may ask for and merges the services' OpenAPI documents into one, so a system has a single page to read rather than one per service.

And the spans are real now. telemetry otlp exports to a collector, a service adopts the trace it arrives in, and the gateway hands on its own context so the service behind it is a child rather than a sibling. Verified by running it against a collector and reading what crossed the wire: the same trace id, a different parent, the caller's sampling decision honoured at a 5% ratio, and a payload carrying this service's name. The phase is closed.

A system is now upgradeable as one thing. crabster upgrade at the root assembles the model again from what each service holds and rebuilds the root and the gateway from it, which is what carries a record added inside a service through to the gateway's routing table. Two defects surfaced with it, both older than this phase: a project that gained its first record and was then upgraded lost its whole API, and the root of a system could not be read back at all. Both are fixed and both are held by test.

Two bugs are worth recording because neither was visible to reading or to cargo check. The SDK exports from a thread of its own and finishes with futures_executor::block_on, so an asynchronous HTTP client handed that thread panics on its first batch — and what the logs then show is a warning about provider lifecycle, which points nowhere near the cause. And a sampler that is not parent-based silently drops a trace a caller had already decided to keep.

Tasks

Definition of Done


Phase 14 — Extended persistence🔗

Objective: move beyond V1's SQL-only scope.

Delivered. Its first half: database mongodb selects a backend rather than a fourth dialect, which is ADR-0004's shape: there are no migrations, no foreign keys and no schema to alter, so what differs is which templates run and not which branch inside one.

The API a caller sees is the API the SQL backend serves — every endpoint, every parameter, every refusal, every problem document. An identifier is a number here too, from a counters collection: MongoDB's own is an ObjectId, and letting that through would mean the typed clients, the UI, the OpenAPI document and a gateway's routes all had to ask where a record is kept before knowing what an identifier looks like.

@unique holds as an index built at startup, @versioned with the version in the filter of the update, @filterable with the operators mapped onto BSON's. A reference is refused by the language, with its line: a field that looks like one and is enforced by nothing fails only once the data is wrong.

Held to it by the suite each project generates — thirteen tests, green against a MongoDB the suite starts itself — and by one in this repository that generates, compiles and runs it.

Three defects were found by running it and could not have been found otherwise. price.greaterThan= came back empty, because rust_decimal serialises as a string and a string compared against a Decimal128 matches nothing. @unique was enforced by nothing at all. And a @versioned record had a version column that nothing checked and nothing moved.

And search is in, which closes the phase. search meilisearch gives every record a full-text endpoint answering the same page a list answers, over tables and over documents alike — what is indexed is the document the API answers with, so the module never asks where the records are kept. Fifteen generated tests green on each backend.

Two things written down here were wrong until running them said so. "A search right after a write sees it" was false by about a tenth of a second, which is measured and now documented rather than waited on. And the generated test passed on the document backend before that backend had any indexing at all: one Meilisearch is shared by whoever is testing, and the suite was searching an index a previous run had filled.

And a generated suite starts the servers it tests against, then takes them away. It used to search a fixed address, which made it pass on the machine where somebody had started an engine by hand and fail everywhere else. The containers, meanwhile, were never removed — a handle in a static is never dropped, so the cleanup that was written never had its chance, and twelve of them were found running on one machine. A reaper now holds the read end of a pipe: the kernel closes it however the run ends, SIGKILL included, where a destructor no longer runs.

Tasks

Definition of Done


Phase 15 — Reverse engineering an existing schema🔗

Objective: lower the entry cost for teams that already have a model (takes over the former Phase 13 of the V1 roadmap).

Delivered. crabster introspect <url> writes a .cdl from a database that already exists, across all three. It is the one command in Crabster that connects to one — everything else computes from the model, and the generated project is what runs a migration.

The Definition of Done asks for an "equivalent" .cdl, and comparing two files as text is the weaker reading. What it is held to instead is that the model which comes back builds what the model that went in built: the test goes round twice — model, database, model, database — and compares the two databases, table by table and column by column.

What cannot come back is written at the top of the file rather than in a document nobody opens, and a second test asserts the difference is exactly that list and not one item more. One real defect surfaced doing it: SQLite records an exact decimal as real(19, 4), so reading the type name alone turned every Decimal into a Float.

Tasks

Definition of Done


Phase 16 — Frontend: typed client & UI blueprints🔗

Objective: apply ADR-0003, which is settled.

No longer waiting on anything. ADR-0003 was accepted on 2026-09-13 and its provisional status dropped: the reconfirmation it asked for waited on usage feedback that cannot exist before a release, and it was blocking this phase and the session half of Phase 12b with it.

The TypeScript client is in. client typescript on a service generates clients/typescript/: a publishable package with no dependencies, covering every endpoint the model generates, with If-Match carried for a versioned record and an ApiError holding the problem document a refusal answers with. Verified by compiling it under strict and running it against the server — create, list, find, patch, a stale version answered 412, a missing record 404, a referenced one refused 409 — and by moving the model and watching the types move with it.

It is generated from the model rather than from the OpenAPI document that same model produces. The two agree by construction; going through the document would put a JSON parse and a code generator between them, both of which can be wrong.

And the Rust client with it. client rust generates its own crate under clients/rust/, which a sibling service depends on by path — the use that makes it more than the TypeScript client in another language, since a system generated here is services calling services. Verified against the running server the same way. client both asks for both, which needed a module's activation to answer to more than one value.

And the official UI blueprint is in, which closes the phase. React and TypeScript, in crates/crabster-codegen/blueprints/react, over the typed client rather than beside it. Official is testable rather than asserted: the gate generates it, installs it, builds it — tsc over the generated pages and the generated client together — and then renders it in a real browser engine, asserting that rows the API holds are in the DOM. Only a mounted React tree that called the generated client could have put them there.

The framework was Samuel's call, taken on the criterion ADR-0003 names as far as it could be: React is where the largest audience is and what a reader arriving from JHipster expects. It is a blueprint and not core, which is the decision itself — a project that wants a UI gets one, and a project that does not is untouched by React's next major.

Tasks

Definition of Done


Phase 17 — Kubernetes & cloud deployment🔗

Objective: cover deployment beyond docker-compose.

Delivered. deploy kubernetes generates two things that are not a mistake for each other. k8s/ holds manifests with every value already filled in — the port the model asked for, the probes the service actually serves, the image its own Dockerfile builds — so kubectl apply -f k8s/ is the whole instruction. chart/ holds the same deployment as a Helm chart, for a team that already has a way of shipping: values to override per environment, a release name, a revision to roll back to.

No sub-generator per provider, which is the restraint the phase asked for. What EKS, GKE and AKS need that a conformant cluster does not is an ingress class, a storage class and a way to push an image: three values. Three generators to keep working would cost far more, and would rot between the day they were written and the day you read them. The generated k8s/README.md names those three.

Verified by deploying it: a generated project runs on minikube behind ingress-nginx, and a record created through the Ingress is read back through it. Every manifest was also put to a real 1.34 API server, and the chart through helm lint and helm template.

Two things only running could have said.

The first was mine: the README claimed a local cluster shares the daemon that built the image. It does not — a cluster's image store is its own, so a freshly built image is ImagePullBackOff while docker run finds it at once, and the error says the repository does not exist, which reads like a typo in the tag. There is now a one-liner per cluster.

The second was not mine and is worse. Two replicas that start together migrate together, and PostgreSQL fails the loser with a duplicate key on pg_type_typname_nsp_index — an index nobody wrote, naming nothing a reader of a generated project has ever heard of. The pod exits 1, Kubernetes restarts it, the other has finished by then: the rollout "succeeds" while crashing every time. Two at once is not exotic — it is a Deployment with two replicas, a scaled Compose file, and every rolling release. Migrations now run behind a lock the database itself hands out, on a connection of their own. Measured: the same rollout, from an empty namespace, reports no restarts at all.

The multi-service half is in. A model with several service blocks now generates, above the projects, a k8s/ that deploys the whole system into one namespace — one, which is the decision worth stating: these services reach each other by name, and a name resolves inside a namespace without qualification. Splitting them would make every address service.namespace.svc.cluster.local, generated into configuration files, and wrong the first time anybody deploys the system twice. Deploying it twice is what the namespace is for: change that one name, apply again, and the second system reaches its own services.

Consul and Jaeger are there when services ask for them, a database per service that keeps anything — never shared, because a service that reads another's tables is not a service but a module with a network hop — and one way in: the gateway if there is one, otherwise a host per service.

Writing it surfaced a trap: a Kubernetes object name may not contain an underscore, while the gateway generates its addresses from the model's raw names. A service the model calls catalog_api answers here to catalog-api, and the routing table baked into its image would point at nothing — a 502 on every path that service owns, with nothing to explain it. So the root generates the routing table as an override, mounted over the image's config/prod.toml. Over the one file and not the directory, because config/default.toml has to survive beside it. It is also the only way: a list is the one shape the configuration cannot receive through an environment variable, which the gateway's own template already said.

And a system deploys more than once. k8s/ gains a kustomize base and overlays/ a worked example: a second cluster is a directory saying what differs, not a copy of the manifests that will drift. Four things differ between clusters and nothing else in a generated system does — the namespace, where the images come from and which, how many of each, and the way in. Verified by deploying both copies into one cluster: they do not meet, which is the property that makes the second cluster credible.

It is not one system spread across clusters, with a service here calling a service there. That needs a mesh — Istio, Linkerd, Cilium, Submariner — and which one is a decision about your network rather than your model. Generating for one would be the sub-generator per vendor this phase refuses.

And what closes the phase: the definition of done is now run rather than remembered. Everything above had been verified by hand, once — and a verification done once stops the day somebody edits a template. Two tests hold it now.

a_system_deploys_to_a_cluster_and_answers_through_its_ingress does exactly what the DoD asks: it generates a system of two services and a gateway, builds all three images from their own Dockerfiles, loads them into the cluster's image store, applies k8s/ with -k, waits for the three rollouts, then creates and reads a record back through the Ingress — for both services, at the same address and under the same host. Nothing in it edits what was generated, which is the other half of the sentence. Forty seconds with the images cached, and a guard takes the namespace away even when the test blows up in the middle.

What it catches and nothing else did: between a manifest and a served request lies everything a render cannot see. Proved by pointing the Ingress at a class nobody serves — every object deploys, all three rollouts succeed, and nothing answers any more. kubectl kustomize found that YAML perfectly fine.

And the_chart_lints_renders_and_takes_the_values_it_offers holds the other half, the chart: helm lint, the render, the model's port arriving in the three places that have to agree, a --set that actually changes the render — without which the chart has quietly become a copy of the manifests beside it — and the invariant nobody looks at: turning the autoscaler on has to take replicas: away from the Deployment, or both own that number and the pod count oscillates for reasons that appear nowhere.

Tasks

Definition of Done


Phase 18 — Blueprint ecosystem maturity🔗

Objective: turn the mechanism shipped in V1 into a real ecosystem.

Begun with what was missing most: knowing what a blueprint can be broken by. What it is written against is five kinds of name — the modules it requires, the files they generate, the slots inside them, the protected regions, the context variables — and none of them was declared anywhere as a promise. Renaming a slot is a one-line edit inside one module, every test still passes, and every blueprint that named it breaks later, on somebody else's machine, with a message about a contribution that reached nothing.

blueprint-contract.txt lists them all, read out of the templates themselves, and a test compares them against what the templates say today, naming what went. It does not judge the change — before 1.0 a minor version may break anything, and CONTRIBUTING.md now says which change demands what. It makes it visible to whoever makes it, at the moment they make it.

And the contributor tooling: crabster blueprint new writes a blueprint that generates — a scaffold that does not run is worse than no scaffold, because the author changes it, it fails, and they cannot tell whether the fault is theirs. crabster blueprint check answers without generating anything the question generation would only ask too late: does this blueprint still name things that exist. crabster blueprint contract prints what there is to name.

And the third official blueprint is in. examples/blueprints/diesel replaces SeaORM with Diesel, on SQLite and on PostgreSQL: nine files carry the whole change, while the OpenAPI document, the validation, the page contract and everything core generates are inherited untouched. That is what makes the mechanism interesting rather than merely possible — the choice of ORM was never something Crabster had to impose.

SQLite, because Diesel's other backends link against libpq or libmysqlclient, which a generated project cannot assume are installed — least of all inside its own rust:slim image. What it does not implement it refuses: @filterable, @versioned, @audited, references, enumerations, Decimal and Uuid produce a compile_error! naming the feature and the record.

Two defects found by running it, both invisible to reading. The generated DATABASE_URL is the one sqlx reads, which Diesel cannot open. And {"field": null} answered 200 and cleared nothing: Diesel reads a None in a changeset as "leave this column alone". The generated suite passed throughout, because nothing asked.

And the showcase is in, which closes the phase. The table of known blueprints carries every one of them, with the templates version each targets, and a test compares it against what the repository actually ships — both ways: a blueprint added and not listed is one nobody finds, and a row naming a version the blueprint does not declare sends a reader to something Crabster refuses to load, talking about the version rather than about the page that was wrong.

The crates.io convention existed on paper; crabster blueprint new now writes it. A convention a document merely asks for is followed half the time. And the round trip — scaffold, package, unpack elsewhere, generate — is checked rather than assumed.

What is left: a blueprint maintained by a contributor outside the project. That does not wait on code. And the definition of done asks for at least one blueprint maintained by a contributor outside the project, which no work on this repository can produce: it waits on people, not on code.

Tasks

Definition of Done


Phase 19 — Interactive project creation🔗

Objective: crabster new asks, rather than being told in flags — the experience the Java ecosystem set the expectation for.

Governed by ADR-0006, which is the whole design: the wizard is a façade over the flags, answers are recorded in .crabster/project.toml and the service block, and no question is asked that the generator cannot answer. It is ongoing rather than sequential — it starts with the questions that already have answers, and every phase above adds its own as it lands.

The mechanism is delivered. crabster new with no arguments asks for the name, the database and the port, and produces the project the equivalent flags produce, byte for byte. It asks only when stdin is a terminal, and --no-input turns it off even then — so a pipe, a CI runner and this repository's own suite are unaffected, which is checked rather than assumed. Line-based and numbered rather than arrow-key driven, which is why it added no dependency.

And the architecture question is in. crabster new asks whether you want one application or several; answer several and it asks for a gateway, the services, whether they register with a catalogue and whether they export spans, then writes the .cdl and generates the system from it. Everything Phase 13 built is now reachable without writing a model by hand.

The design is what makes it worth having: the questions produce a model, and the model goes through the path crabster import-cdl already took. There is no second generator to keep in step. Held to it by test — the system answered for, the system the equivalent flags make, and the system imported from the .cdl those questions wrote are the same files with the same bytes — and verified by running one: a wizard-made system brought up with Compose, a record added afterwards, created and listed back through the gateway, both services passing in Consul, and one trace with the service a child of the gateway.

Two things came out of building it. A service declared before its first record is now a project rather than an error — that is what a system starts as — and it is generated without the record machinery, so it compiles without the warnings a listing type nothing calls produces. And a gateway routes what the model says a service owns, so one created before any records routes nothing — which is what upgrading a system as a whole closes, in Phase 13 above.

And the questions that were missing are there, which closes the phase. crabster new now asks for the front end — none, React, Vue or Angular — and for accounts, which is ADR-0006 applied as written: a question is asked the day the generator can answer it. That day arrived for two separate reasons, and neither was "there was time". The three UI blueprints ship in the binary, so the answer selects something that exists on the machine that installed the command. And new can write a model for a single application, as it already did for a system: auth and ui both generate against records, so they arrive with a first record — asked for, with a default that can be typed over, never invented in silence. With no terminal to ask on and no --record, it is a refusal that names the flag.

What set this off was not a phase but an attempt: crabster new block handed twenty files to somebody who expected an application, and was right to expect one. A bare crabster new now gives an API, a database, accounts and a screen — and the .cdl stays in the project, because the model is the thing that grows.

Tasks

What it does not settle

Definition of Done

6. V2 exit criteria🔗

V2 is considered shipped when:

  1. A generated project can be upgraded to a new template version without code loss (Phase 11) — blocking criterion.
  2. OAuth2/OIDC is production-usable as a resource server (Phase 12a).
  3. A multi-service deployment works end-to-end (Phases 13 + 17).
  4. An existing database can be taken over (Phase 15).
  5. A typed client is generated from the API (Phase 16).
  6. The blueprint ecosystem has at least one external contributor (Phase 18).
  7. crabster new asks for what it needs rather than being told (Phase 19).

Phase 14 (extended persistence) and part of Phase 17 may slip to V3 without invalidating the V2 release, should resource trade-offs require it.

7. Explicitly out of V2 scope (V3 horizon)🔗

8. V2-specific risks🔗