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🔗
| V1 | V2 | |
|---|---|---|
| Promise | "From a domain model to a production Rust API in 5 minutes" | "A Crabster project lives, evolves, and ships over time" |
| Generation | One-shot + simple record addition | Incremental regeneration, version upgrade, assisted merging |
| Application scope | API-only monolith, SQL, JWT | Multi-service, NoSQL, OAuth2/OIDC, frontend (decision settled) |
| Ecosystem | Blueprint mechanism shipped, ecosystem empty | Blueprint ecosystem bootstrapped, official blueprints maintained |
| Migration | New projects only | Reverse 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🔗
| Theme | Phases | Motivation |
|---|---|---|
| T1 — Generated code lifecycle | 11 | Make a Crabster project maintainable over time |
| T2 — Enterprise-grade security | 12a, 12b | Remove the OAuth2/OIDC blocker for enterprise adoption |
| T3 — Distributed architectures | 13 | Multi-service generation |
| T4 — Extended persistence | 14 | Move beyond SQL only (NoSQL, search) |
| T5 — Migration & interoperability | 15 | Take over existing databases |
| T6 — Frontend | 16 | Resolve ADR-0001, deferred since V1 |
| T7 — Deployment & ecosystem | 17, 18 | Kubernetes, cloud, community blueprint maturity |
| T8 — Creation experience | 19 | Ask 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🔗
- Status: implemented, with one deviation. Both mechanisms ship: protected zones, and the snapshot in
.crabster/snapshot/with the three-way merge it enables, incrabster upgrade. - Deviation: the conflict markers do not go into the file. The decision said "
git merge-style conflict markers"; whatcrabster upgradedoes is leave the file exactly as its owner left it and write the marked merge to.crabster/incoming/under the same path. Git can afford to write markers into a working tree because it is the way back; a generated project need not be in git at all, and a tool that writes into a file it did not write must leave the owner something to return to. The information is the same and it is one copy away. - Context: V1 generates one-shot and refuses to overwrite a hand-modified file. For V2, we need to regenerate a project whose code the user has modified, without losing those modifications.
- Decision: a hybrid approach with two complementary mechanisms:
- Protected zones — generated files carry comment markers delimiting user-reserved regions (reserved-zone markers), preserved verbatim across regeneration.
- 3-way merge — for everything else, Crabster keeps a snapshot of the generated code as of the last generation inside the project (
.crabster/). An update compares old generated / new generated / current user code and produces a merge, withgit merge-style conflict markers when automatic resolution is impossible.
- Consequences: the
.crabster/directory (CDL model + snapshot + template version used) becomes a versioned artifact of the generated project, to be clearly documented. Templates must be designed to minimize conflicts (separating purely generated code from code meant to be edited). - Alternative ruled out: full regeneration with manual
git diffleft to the user — simple to implement, but shifts the entire cost onto the user and does not scale to a real project.
ADR-0003 — Frontend: generated typed client, then UI blueprints🔗
- Status: accepted (2026-09-13). This ADR formally closes ADR-0001.
- Context: ADR-0001 deferred the frontend choice. Three options are on the table: (a) generate a full JS/TS frontend, (b) generate a Rust/WASM frontend (Leptos/Yew), (c) generate only a typed client from the OpenAPI spec, with no UI.
- Decision: (c) in the core — generate a typed client (TypeScript and Rust) from the OpenAPI contract stabilized in V1 — with (a) and (b) as official blueprints rather than core.
- Rationale: (a) and (b) mean maintaining one or more UI frameworks across their breaking-change cycles — the single heaviest maintenance cost such generators carry, and one paid in a second ecosystem's terms: framework, bundler and lockfile, each on its own release cadence. This repository already carries ten template modules that have to stay in step across a version bump, and
crabster upgrademerges generated code from one templates version to the next. Adding a UI to the core doubles that surface. Option (c) delivers most of the value — end-to-end typing, no hand-written HTTP client — at a fraction of the cost, and stays useful whatever UI framework is picked.
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.
- Consequences:
- Crabster does not promise a turnkey UI application. This is the cost, and it is worth naming plainly: it is the one place where Crabster is not JHipster-equivalent, and a reader arriving from that ecosystem will notice on the first day. It has to be said in the project's own words rather than discovered.
- The creation wizard (ADR-0006) asks for the front end, and since 2026-09-17 it offers none, React, Vue or Angular — the three blueprints shipped in the binary. The rule held: the option appeared the day it worked, and not before.
- "Official blueprint" has to mean something testable, or it is a word. An official blueprint lives in this repository, is generated and compiled in the heavy tier, and is held to the same gate as the core. A blueprint that is merely linked to is a community blueprint, and should be called one.
- Which framework the first official UI blueprint targets is not settled here and does not need to be: it is a question about who turns up, and the criterion is which ecosystem the first users are already in.
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🔗
- Status: proposed.
- Context: SeaORM covers SQL. MongoDB (and any document database) does not fit its model.
- Decision: introduce a "persistence backend" abstraction in the IR and treat MongoDB as a parallel implementation (native
mongodbdriver), not as yet another dialect behind SeaORM. Record templates specialise per backend. - Consequences: some CDL constructs are meaningless on a document store — references, which become foreign keys, and schema migrations; the parser must explicitly reject those combinations with a clear message rather than generate incoherent code.
ADR-0005 — Microservices without a Spring Cloud equivalent🔗
- Status: proposed.
- Context: other ecosystems ship an integrated distributed stack — registry, config server, gateway. Rust has no equivalent.
- Decision: compose existing building blocks rather than rebuild a stack: Consul for service discovery and centralized configuration (Consul KV), and a generated Axum + tower gateway rather than a third-party product.
- Consequences: fewer "turnkey" microservices features, but no dependency on a proprietary distributed framework. Service meshes (Istio/Linkerd) are documented as the recommended alternative for advanced needs, outside the generation scope.
ADR-0006 — Interactive creation, as a façade over the flags🔗
- Status: accepted (2026-09-10).
- Context: the target is a creation wizard in the manner of JHipster —
desktop or web; if web, monolith or microservices; then the authentication
method, the frontend client, the gateway. Today nothing in the CLI reads a
terminal:
crabster newtakes flags,crabster import-cdlreads a.cdl, and theserviceblock carries the rest. The wizard is not a rewrite of that; the question is what it is allowed to be. - Decision: four rules, and the first is the one the others follow from.
- The wizard is a façade over the flags, never the only path. Every
answer it collects has a flag or a
servicesetting that says the same thing, and a non-interactive invocation skips it entirely. - Answers are recorded where the lifecycle already looks —
.crabster/project.tomland the model'sserviceblock. Nothing a wizard asks may live only in the asking. - No question without a generable answer. A question whose options do not all exist yet is not added until they do: offering a choice the generator cannot honour is worse than not offering it.
- Each phase adds its own question, in the commit that delivers the capability behind it.
- The wizard is a façade over the flags, never the only path. Every
answer it collects has a flag or a
- Consequences:
- It can ship now, asking only what already has an answer — database, port, and the authentication method — and grow one question per phase.
- The test suite and CI keep driving the CLI without a terminal. This is not
a detail: fourteen end-to-end tests compile and run whole generated
projects, and the regression corpus drives
record,applyandupgradeby argument. A wizard that became mandatory would break all of them on the day it landed. - It settles the mechanism and not the menu. Two of the questions the target
names are not Crabster's to answer yet, and are open here rather than
decided:
- Desktop is outside the documented product. Every phase from 0 to 18 assumes an HTTP service — records become a CRUD API, OpenAPI, health probes, an image, a pipeline. A desktop application keeps the domain model and little else. It is a second product line, not a phase, and it needs a decision of its own before it can be a question.
- The frontend client is governed by ADR-0003, which keeps UI frameworks out of the core and puts them in blueprints. The two reconcile without reversing it: the wizard asks, and the answer selects a blueprint. The experience is the one the target describes; the core still carries no JS framework through its breaking-change cycles.
- Alternatives ruled out:
- The wizard as the only path. It is how the tool would be discovered, and
it is also how it would stop being scriptable. Every generator that has
done this has had to add a
--skip-promptsafterwards. - Answers kept in a file of their own. A second source of truth beside
.crabster/, which is the thing Phase 11 exists to prevent.
- The wizard as the only path. It is how the tool would be discovered, and
it is also how it would stop being scriptable. Every generator that has
done this has had to add a
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.
| Phase | Title | Depends on | Priority |
|---|---|---|---|
| 11 | Lifecycle: upgrade & incremental merging | Full V1 | Critical |
| 12a | Extended authentication: OAuth2/OIDC resource server | 11 | High |
| 12b | Declarative authorization, and the browser session | 12a, 16 | Medium |
| 13 | Microservices | 11, 12a | High |
| 14 | Extended persistence (NoSQL, search) | 11 | Medium |
| 15 | Reverse engineering an existing schema | V1 (Phase 3) | Medium |
| 16 | Frontend: typed client & UI blueprints | V1 (Phase 6) | Medium |
| 17 | Kubernetes & cloud deployment | 13 | Medium |
| 18 | Blueprint ecosystem maturity | 11 | Ongoing |
| 19 | Interactive 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); andcrabster apply, which turns a model that changed into new migrations. A@lengthbound 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.rscarries a realistically modifiedshopacross a templates version and names the fate of every file, andevery_change_a_database_with_rows_can_be_toldapplies 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@uniquetaken away on the two databases that named the constraint themselves.
Tasks
- Define the
.crabster/directory format: current CDL model, generated-code snapshot, template version, file fingerprints. - Implement protected zones: marker convention, extraction/reinjection during regeneration, dedicated tests.
- Implement the 3-way merge (old generated / new generated / user code), producing readable conflict markers.
- Implement
crabster upgrade: detect the project's template version, regenerate, merge, and print a summary report (files unchanged / merged / conflicted). - Extend
crabster record: handle modification and deletion of a record, not just addition, including the corresponding schema migration. - Revisit V1 templates to minimize conflict surface (isolate code meant to be user-edited).
- Build a regression test corpus: generated projects, realistically modified, then upgraded — verifying modifications survive.
Definition of Done
- An example project generated on V1, manually modified (business logic added to handlers and services), then upgraded to V2 templates, retains 100% of user modifications, or explicitly reports conflicts without ever silently overwriting.
- Adding a field to an existing record in the
.cdlfile and re-running generation produces the corresponding migration and updates the code without breaking the project.
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, notauthenticationType oauth2as this document said before theserviceblock 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-oauth2module, discovery and a cached key set, validation against a pinned algorithm, theAuthenticatedextractor, configurable claim extraction, a Keycloak in the generateddocker-composewith 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:
Authenticateddocumented that it could be taken in a handler the model generated, and could not.AppStatetakes fields from a module that needs them now.
Tasks
- The
auth-oauth2module: manifest and activation, the names it reserves, and the_into/slots — the eightauth-jwtalready occupies are the map. .well-knowndiscovery, JWKS fetch, and a cache that survives key rotation without a network call per request.- Token validation:
iss,aud,exp,nbf, and a pinned algorithm. - The
Authenticatedextractor, under the same signatureauth-jwtgives it, so a handler written by hand survives a change of authentication type. - Configurable claim extraction. Keycloak puts roles in
realm_access.roles, Entra ID inroles, Auth0 in a namespaced claim: without this, "two providers" cannot be done at all. It is configuration, not code. - Role hierarchy, and whether it is declared by the project or read from the provider.
- A
docker-composecarrying a Keycloak with a realm imported from JSON — a realm configured by hand is neither reproducible nor testable. - Integration tests against that Keycloak.
testcontainers-modulesis already a dev-dependency of every generated project. - A cloud provider (Auth0, Entra ID) as a verified manual procedure, not as a test: it needs credentials CI cannot hold, and saying so is better than a test that is skipped in silence.
- The CI job. There is none for authentication today —
auth jwtis covered only by the#[ignore]tier. - The question this phase adds to the wizard, per
ADR-0006:
authgains a second value. - The language reference in both languages, where the
authrow listsjwtalone.
Definition of Done
- A project generated with
auth oauth2authenticates end-to-end against a Keycloak started by the generateddocker-compose, with automated integration tests. - The same model, generated with
auth jwtand withauth oauth2, protects a hand-written handler with the same line of code.
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,@readsand@writeson 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 is401, a token carrying a role the record does not name is403, aMANAGERreads what@reads(MANAGER, ADMIN)allows and is refused what@writes(ADMIN)does not, andADMINwrites both. The same model generates the same guard underauth jwtandauth oauth2, which is checked rather than assumed.Three things came with it. A guarded model in a project with no
authsetting 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 againstAuthenticated,ClaimsandAuthErrorwithout knowing which is running. Andauth oauth2can 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 cookieon a service withauth oauth2makes it a backend-for-frontend:/auth/loginredirects to the provider with Authorization Code and PKCE,/auth/callbackexchanges 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
- Per-resource permissions declared in CDL rather than called by hand. Today a
route is protected by editing its handler and writing
claims.require_role("ADMIN"); the alternative is an attribute on the record. - The parser, IR and validation changes that requires, and the refusals that go with them — a role named nowhere is a typo, and should be refused by name.
- Session-based support, lifting the V1 exclusion: relevant only once a UI blueprint exists to hold the session.
- The Authorization Code + PKCE flow, for a backend that also serves the browser (BFF).
Definition of Done
- A record carrying an authorization attribute generates a handler that enforces it, and a model naming a role nothing declares is refused with the line it was written on.
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
serviceblocks generates one project per service,@servicedistributes 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 keepsrecord,applyandupgradeworking on one of them unchanged.A distributed generation now writes a root above the services too: one
docker-composethat 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 gatewayon 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 consulregisters 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 fororderspointed 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 otlpexports 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 upgradeat 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 withfutures_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
- Extend CDL: a service type (
monolith/microservice/gateway), description of multiple applications in one file, record distribution across services. - Generate an Axum +
towergateway: routing to services, OpenAPI documentation aggregation, centralized rate-limiting, authentication context propagation. - Integrate Consul: registration at startup, health checks, gateway-side resolution, centralized configuration via Consul KV.
- Distributed tracing propagation across services (correlation via
tracing+ OpenTelemetry, building on V1's Phase 9). - Multi-service reference example tested in CI (at least two services + a gateway).
Definition of Done
- The multi-service example starts with a single
docker-compose up, services register with Consul, and a request through the gateway reaches the right service with its authentication context and a propagated trace ID.
Phase 14 — Extended persistence🔗
Objective: move beyond V1's SQL-only scope.
Delivered. Its first half:
database mongodbselects 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
counterscollection: MongoDB's own is anObjectId, 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.
@uniqueholds as an index built at startup,@versionedwith the version in the filter of the update,@filterablewith 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, becauserust_decimalserialises as a string and a string compared against aDecimal128matches nothing.@uniquewas enforced by nothing at all. And a@versionedrecord had a version column that nothing checked and nothing moved.And search is in, which closes the phase.
search meilisearchgives 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
staticis 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,SIGKILLincluded, where a destructor no longer runs.
Tasks
- Introduce the persistence backend abstraction in the IR (see ADR-0004) and refactor record templates accordingly.
- MongoDB support: record templates, repository, absence of migrations (document the application-level schema evolution strategy).
- CDL validation: explicit, instructive rejection of impossible combinations (SQL relationships on a document backend).
- Search engine integration — Meilisearch preferred over Elasticsearch for its lighter footprint and Rust integration, to be confirmed at the start of the phase.
- Extend the CI test matrix to the new backends.
Definition of Done
- A suitable CDL model generates a working MongoDB project, with CRUD and green integration tests.
- A model with
searchenabled exposes working search endpoints on the reference example.
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.cdlfrom 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 everyDecimalinto aFloat.
Tasks
- Reverse-engineering mode:
crabster introspectgenerates a.cdlfile from an existing SQL schema (based on SeaORM introspection). - Document what introspection can take over, and the manual follow-up for the rest.
Definition of Done
- Introspecting a database from the
shopexample reproduces a.cdlfile equivalent to the original.
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 typescripton a service generatesclients/typescript/: a publishable package with no dependencies, covering every endpoint the model generates, withIf-Matchcarried for a versioned record and anApiErrorholding the problem document a refusal answers with. Verified by compiling it understrictand running it against the server — create, list, find, patch, a stale version answered412, a missing record404, a referenced one refused409— 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 rustgenerates its own crate underclients/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 bothasks 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 —tscover 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
- Generate a typed TypeScript client from the project's OpenAPI spec, publishable as a package, automatically kept in sync on each regeneration.
- Generate a typed Rust client (useful for integration tests and Phase 13's inter-service communication).
- Publish an official reference UI blueprint to validate the "blueprint" path — framework choice settled at the start of the phase on the criterion ADR-0003 names: which ecosystem the first users are already in. Official means generated and compiled in the heavy tier, like the core.
Definition of Done
- The generated TypeScript client compiles, is typed end-to-end, and a CDL model change propagates through to the client's types after regeneration.
- At least one working UI blueprint is published and documented.
Phase 17 — Kubernetes & cloud deployment🔗
Objective: cover deployment beyond docker-compose.
Delivered.
deploy kubernetesgenerates 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 ownDockerfilebuilds — sokubectl 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.mdnames 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 lintandhelm 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
ImagePullBackOffwhiledocker runfinds 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
serviceblocks now generates, above the projects, ak8s/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 addressservice.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_apianswers here tocatalog-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'sconfig/prod.toml. Over the one file and not the directory, becauseconfig/default.tomlhas 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 andoverlays/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_ingressdoes exactly what the DoD asks: it generates a system of two services and a gateway, builds all three images from their ownDockerfiles, loads them into the cluster's image store, appliesk8s/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 kustomizefound that YAML perfectly fine.And
the_chart_lints_renders_and_takes_the_values_it_offersholds the other half, the chart:helm lint, the render, the model's port arriving in the three places that have to agree, a--setthat 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 takereplicas:away from the Deployment, or both own that number and the pod count oscillates for reasons that appear nowhere.
Tasks
- Generate Kubernetes manifests (Deployment, Service, ConfigMap, Secret, Ingress, HPA) and a Helm chart.
- Support Phase 13's multi-service deployment (gateway + services + Consul).
- Document common cloud targets without generating provider-specific configuration (a restraint choice: per-provider sub-generators are a high maintenance burden).
Definition of Done
- The multi-service example deploys to a local cluster (kind/minikube) from the generated manifests alone, and responds through the Ingress.
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.txtlists 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, andCONTRIBUTING.mdnow 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 newwrites 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 checkanswers without generating anything the question generation would only ask too late: does this blueprint still name things that exist.crabster blueprint contractprints what there is to name.And the third official blueprint is in.
examples/blueprints/dieselreplaces SeaORM with Diesel, on SQLite and on PostgreSQL: nine files carry the whole change, while the OpenAPI document, the validation, the page contract and everythingcoregenerates 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:slimimage. What it does not implement it refuses:@filterable,@versioned,@audited, references, enumerations,DecimalandUuidproduce acompile_error!naming the feature and the record.Two defects found by running it, both invisible to reading. The generated
DATABASE_URLis the one sqlx reads, which Diesel cannot open. And{"field": null}answered 200 and cleared nothing: Diesel reads aNonein 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 newnow 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
- Stabilize and version the blueprint API (the contract between core and external templates), with an explicit compatibility policy.
- Publish the identified official blueprints: Diesel (ORM alternative), UI (Phase 16), and any blueprint arising from reported needs.
- Contributor tooling:
crabster blueprint newto scaffold a blueprint, plus a test harness for blueprints. - Community showcase (page of known blueprints, crates.io tag convention).
Definition of Done
- At least three working blueprints exist, including at least one maintained by a contributor outside the project.
- The blueprint API is versioned and breaking changes to it are automatically detected in CI.
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.tomland theserviceblock, 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 newwith 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-inputturns 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 newasks 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.cdland 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-cdlalready 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.cdlthose 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 newnow 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. Andnewcan write a model for a single application, as it already did for a system:authanduiboth 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 blockhanded twenty files to somebody who expected an application, and was right to expect one. A barecrabster newnow gives an API, a database, accounts and a screen — and the.cdlstays in the project, because the model is the thing that grows.
Tasks
- The prompt layer itself, and a
--no-inputthat skips it whole. Every question has a flag; the flag being given is what suppresses the question. - The first three questions, which already have answers: the project name, the
database, and the port. Not the authentication method:
crabster newcreates a project without a model, and there is no authenticating a project with nothing to protect. - Non-interactive detection, so a pipe or a CI runner never blocks on a prompt waiting for a terminal that is not there.
- The answers written where the lifecycle reads them, so a project created by the wizard is indistinguishable from one created by flags — the corpus should not be able to tell.
What it does not settle
- Desktop applications. Not a phase: every phase from 0 to 18 assumes an HTTP service, and a desktop application keeps the domain model and little else. It needs a decision on the product before it can be a question in the wizard.
- The frontend client question, which belongs to ADR-0003 and Phase 16. When it is asked, the answer selects a blueprint — the wizard offers the choice, the core carries no UI framework.
- The monolith/microservices question, which cannot be asked before Phase 13 can answer it.
Definition of Done
crabster newwith no flags walks through the questions and produces the same project the equivalent flags produce, byte for byte.- The whole test suite still drives the CLI without a terminal.
6. V2 exit criteria🔗
V2 is considered shipped when:
- A generated project can be upgraded to a new template version without code loss (Phase 11) — blocking criterion.
- OAuth2/OIDC is production-usable as a resource server (Phase 12a).
- A multi-service deployment works end-to-end (Phases 13 + 17).
- An existing database can be taken over (Phase 15).
- A typed client is generated from the API (Phase 16).
- The blueprint ecosystem has at least one external contributor (Phase 18).
crabster newasks 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)🔗
- Desktop applications. Named here because the creation wizard makes the question visible (ADR-0006), and the answer is not "later in V2". Every phase from 0 to 18 generates an HTTP service; a desktop application would keep the domain model and the migrations and replace everything above them. That is a second product line and needs a decision of its own, not a slot in a phase.
- Web-UI generation and a visual model editor.
- Per-provider cloud sub-generators (Heroku, AWS, GCP, Azure) — documented, not generated (see Phase 17).
- NoSQL databases beyond MongoDB (Cassandra, Couchbase, Neo4j).
- Generating non-Rust applications — Crabster remains a Rust generator.
- CQRS/event-sourcing support — too architecture-specific for a general-purpose generator; a natural candidate for a community blueprint.
8. V2-specific risks🔗
- Incremental merging is a money pit. This is risk number one: the problem is intrinsically hard and can absorb all of V2's capacity. Mitigation: aim first for the "never silently lose user code" guarantee (an explicit conflict is a success, not a failure), not for perfect hands-off merging.
- Breaking V1 templates. Phase 11 requires restructuring existing templates; V1-generated projects need a documented migration path, otherwise early adopters pay the price — precisely the population to protect.
- Spreading thin across parallel themes. Phases 12-16 are attractive and visible; Phase 11 is thankless and invisible. There is a real risk of sacrificing it; the priority order in §2 must be defended.
- Dependency on the Consul/Meilisearch ecosystem. These choices commit to external dependencies outside the project's control; isolate them behind blueprint templates so they can be swapped.
- Community pressure on the frontend. ADR-0003 assumes no turnkey UI in the core. This position will be challenged; it must be argued publicly, and revised on usage data, not noise. {% endraw %}