Skip to content

The 30 packages

A framework is usually one package. Elvel is thirty, and this page is why.

PackageContents
@elvel/contractsInterfaces only. Breaks dependency cycles between packages.
@elvel/supportStr, Arr, Collection, Macroable, Conditionable.
@elvel/coreApplication, ServiceProvider, Config, Env, exception handler, helpers.
@elvel/databaseConnections, query builder, models, schema builder and migrator on Bun.SQL.
@elvel/httpFormRequest, JsonResource, sessions, signed and encrypted cookies, CSRF, rate limiting, CORS, trusted proxies.
@elvel/validationTwo-phase validation: ~50 rules, unique/exists, error bags.
@elvel/eventsDispatcher with wildcards, halting, subscribers, EventFake.
@elvel/logChannels and drivers (console, json, single, daily, stack, null).
@elvel/consoleThe CLI: signature parser, command base, kernel, stub generators.
@elvel/viewJSX renderer (@kitajs/html), view()/render() helpers, static file serving.
@elvel/viteThe Vite plugin: hot file, server-side reload, build output, asset URLs.
@elvel/clientThe browser's fetch for its own backend: session cookie, CSRF token, /api prefix, typed failures, useForm.
@elvel/authbetter-auth over our own query builder, plus Gate and policies.
@elvel/cacheFour stores (array, file, database, redis) with atomic locks, tags and a rate limiter.
@elvel/queueJobs, three drivers, a worker with retries and backoff, chains, failed jobs.
@elvel/schedulerCron matcher, withoutOverlapping, timezones, schedule:run/schedule:test.
@elvel/mailMailables, nodemailer transports, queued mail.
@elvel/storageDisks (local, s3 on Bun.S3Client), path guard, offline presigned URLs.
@elvel/notificationsChannels (mail, database, log), per-recipient ids, on-demand recipients.
@elvel/encryptionAES-256-GCM, HKDF-derived keys, context binding, key rotation, key:generate.
@elvel/redisOptional. One manager for every Redis connection, shared by cache, queue and broadcasting, with command timing.
@elvel/lensOptional. Records what a request did, and a dashboard to read it back.
create-elvelApplication skeleton scaffolder.

Why thirty

A one-package framework arrives whole whether or not you touch it, and registering all of Eloquent, Queue and Mail in an application that uses none of them costs nothing extra because the code was already in vendor/.

npm has no such arrangement. Every dependency is downloaded, resolved and — if you bundle — included. Measured, registering all twenty-two providers took a landing page from 1.0 MB to 3.7 MB. So the split is not tidiness; it is the only way an application that wants routing and views can avoid paying for a queue driver it never calls.

What follows from it is the whole shape of the framework:

  • bootstrap/providers.ts names what the application registers, and a starter kit is mostly a different version of that file
  • @elvel/contracts exists so packages can refer to each other's types without importing each other's code, which is what keeps the graph acyclic
  • a command exists only if its package is registered, so bun elvel list differs between two applications
  • create-elvel prunes providers, dependencies and config files per kit — see installation

Design decisions

The container is typed, not stringly-typed

Some frameworks lean on app('cache') + facades; copying that verbatim would destroy Elysia's end-to-end inference, its main advantage. Bindings are declared by augmenting ContainerBindings, so app('view') resolves to a real type. That interface must stay an interface — a type alias cannot be augmented.

Controllers are Elysia instances

This is what Elysia's own docs prescribe, and the only shape that keeps the request context inferred inside handlers. Each controller carries a name so Elysia deduplicates its routes.

Global helpers instead of context decorators

view(), config(), app() resolve from the running application. Decorating the Elysia context instead would force every route to carry those types.

Views are typed JSX, not a template language

@kitajs/html compiles JSX straight to strings — no virtual DOM, ~2-3x faster than React/Preact/Hono JSX at about half the memory. A view is a function, so tsc is the template checker and Bun's module cache is the compile cache: no view paths, no compiled-view directory, and a renamed prop is a compile error instead of a blank page.

Components are passed by reference, never by name:

ts
// app/Http/Controllers/PageController.ts  — stays .ts, no JSX syntax here
import { view } from '@elvel/view'
import { Landing } from '../../../resources/views/pages/landing.tsx'

.get('/', () => view(Landing, { title: 'Welcome' }))

Only files containing JSX syntax need .tsx; a .ts file with a JSX literal is a syntax error in both tsc and Bun. Layouts are components and the page body arrives as children (typed as Children from @kitajs/html). view() prepends <!DOCTYPE html> when the markup opens with <html, since JSX has no doctype node.

Escaping is opt-in

Mark interpolated user input with safe<span safe>{comment}</span> — and it is HTML-escaped at render time. The matching compile-time checker, @kitajs/ts-html-plugin, cannot be wired into bun run verify today: its CLI reads typescript.sys, which TypeScript 7 removed from the default export, so it crashes under both Bun and Node. Until that is fixed, safe is a runtime guarantee and a review responsibility.

Workspace linking, never file: dependencies

Bun hardlinks file: dependencies into its store; an editor that writes by replacing a file detaches the copy, and the app then runs stale code while TypeScript sees two identities of the same module. Apps scaffolded inside this repo become workspace members.

Where the differences are forced

Four, and each has a page:

Why
No facades, no autowiringTypeScript erases the types both depend on — lifecycle
allowGuests on a policyReading a nullable user type is not possible; there is no type at runtime — authorization
A job carries data, not itselfPHP can serialize($job)queues
onSuccess rather than thenA class with a then member is a thenable — queues

Everything else that differs is a choice rather than a constraint, and BEHAVIOURS.md records those with the bug or the measurement that decided them.

MIT. Alpha — the shape is settled, the surface still moves.