Skip to content

Coming from AdonisJS ​

Explanation. AdonisJS is a full-stack framework with a database layer, a CLI, an auth module and a view engine. This framework ships transport authentication, but no bundled account system, model layer, resident CLI or view engine. The useful half of this page is what does not map. For the design, start with Why btravstack?.

Adonis gives you a working application on day one; the parts arrive together and agree with each other. This framework gives you a lifecycle, a container the compiler checks, and starters for transports — the repository, account workflows and views are yours to choose. That is the trade in one line, and everything below is a consequence of it.

Three things that look like they map ​

These are the ones worth reading carefully, because a word means something different on each side.

"Provider" is not a provider ​

An Adonis provider is a lifecycle hook on the application container: register(), boot(), start(), ready(), shutdown(), registered in adonisrc.ts and often reaching into app.container.singleton(...).

Here a provider is one binding: how a single port's service is built, declared as a value and composed into a module. It has no lifecycle methods — what an Adonis provider's shutdown() does is a resourceful provider's release, which the scope runs on every exit path, and what its boot() does is usually just… construction, because a provider is only built when something needs it.

The container-registration half has no equivalent at all: there is no app.container.bind, no string keys and no service locator to reach into. If a piece of code needs a service, it declares it in inject and receives it.

There is no Lucid, and no model layer ​

BaseModel, @column, .find(), .related(), .preload(), migrations through ace, factories and seeders — none of it exists here, and no replacement ships. Persistence is a port your application declares and an adapter you write, usually over Prisma (@btravstack/prisma is the starter that binds one from DATABASE_URL and closes the pool with the scope).

What that costs and buys is worth being blunt about. It costs you the fastest path from an idea to a table — an Adonis model with two decorators is doing work you will write by hand. It buys you a domain layer that does not import the database: OrderRepository is your vocabulary, the Prisma client sits behind it, and swapping the adapter is composing a different module rather than a migration of every call site.

The router is not a file ​

start/routes.ts — router.get('/orders', [OrdersController, 'index']) — is Adonis's map of URLs to controllers, edited by hand.

Here the contract is the map, and it is a value in its own package: an oRPC contract declares each procedure's input, output and errors, the router implements it, and the compiler refuses an implementation that does not match. Nothing routes by string at runtime, and the client imports the same contract and gets the call signatures for free — which is the reason the contract lives in a package a client can take without the server.

The shape that carries over is the controller: a slice owns api.OrpcController(contract, "orders"), which is one contract key's implementation as a provider. See Split a router into controllers.

What has no equivalent ​

No resident ace or per-slice generator ​

The current tooling decision declines a resident CLI and a per-slice generator (#59). Copy a sibling slice and add its contract key in the separate contract package; Add a slice by copying a sibling walks through both edits. Starting a new application is a separate gap: a minimal external template is tracked in #437.

No bundled account or identity system ​

The HTTP starter authenticates API keys, JWTs and session cookies, and its OIDC answerer handles browser login. A contract declares which scheme and scopes a procedure accepts. Account registration, credential issuance, recovery and the identity store remain the application's responsibility. See Protect a procedure and Log a browser in.

No Bouncer, no policies, no @can ​

Authentication is a starter concern — a contract marks which security schemes a procedure accepts, and the application binds an authenticator per scheme, so a scope on the credential is checked before dispatch. Authorization — "may this caller read this order" — is a call in your handler against your own rules, and its result is a witness type the operation demands, so handing the operation a resource with no witness is a compile error — the property a Bouncer policy cannot have, since bouncer.authorize is a call anyone can leave out. There is no policy class, no ability registry and no template directive.

See Authorize a request for the three layers, and Protect a procedure for the first.

No Edge, no views, no i18n ​

The HTML story here is htmx fragments over an Html value that escapes by default — enough for a server-rendered page, and nothing like a view layer. There is no template inheritance, no components, no @each, and no translation files. A rendering library is yours to choose, and it composes as an ordinary provider.

No scheduler ​

Adonis reaches for a scheduler package and a cron expression. Here scheduled work is Temporal's — a Schedule with retries, a history, and a run that happens once across replicas rather than once per replica. It is a heavier answer and a more honest one at three replicas.

One runtime per process ​

An Adonis application serves HTTP, and a queue worker is node ace queue:work beside it. Here each is a deployment of the same image booting a different runtime, and a graph holds exactly one. Three transports means three processes that scale and fail independently. See One process, one runtime.

Exceptions are not the error channel ​

Adonis's exception handler turns a thrown error into a response, and @adonisjs/core/exceptions gives you HTTP-shaped ones to throw. Here a failure is a value: a Result whose error type the compiler tracks, mapped to a status exactly once, in the router's triage. A throw is a defect — reported, never routed. See Nothing throws.

What AdonisJS does better ​

Time to a working application. Everything agrees with everything, the CLI writes the boilerplate, and the database layer is right there. For a solo project that ships this month, that is a serious advantage.

Batteries. Auth, validation, mail, views, queues, tests and the CLI, all one team's decisions. Here you assemble that from starters and libraries, and some of those choices are still yours to make.

Where to go next ​

Released under the MIT License.