Backends, compiled.
Heximon reads your plain TypeScript classes and wires the whole application before it boots — routing, dependency injection, validation, typed clients. No decorators. No runtime container. Nothing left to guess at runtime.
export class OrdersModule extends Module({ providers: [OrdersService, OrdersRepository], http: { controllers: [OrdersController] }, events: { handlers: [OrderPlacedHandler] },}) {}No magic — provably
Read what it writes.
cat the proof: the compiler emits plain JavaScript into .heximon/ — constructors called in dependency order, cross-module dependencies passed in as plain arguments, nothing else. Read it in review. Diff it. Set a breakpoint in it. import { Module } from "@heximon/runtime";
export class OrdersModule extends Module({
imports: [PersistenceModule],
providers: [
OrdersService,
OrdersRepository,
PaymentGateway,
],
http: { controllers: [OrdersController] },
events: { handlers: [OrderPlacedHandler] },
}) {}export class OrdersService {
// The constructor is the dependency declaration.
constructor(
private readonly orders: OrdersRepository,
private readonly payments: PaymentGateway,
) {}
}// GENERATED by Heximon — do not edit.
import { PaymentGateway } from "../src/payment.gateway.js";
import { OrdersRepository } from "../src/orders.repository.js";
import { OrdersService } from "../src/orders.service.js";
import { OrdersController } from "../src/orders.controller.js";
import { OrderPlacedHandler } from "../src/order-placed.handler.js";
export async function initOrdersModule(inputs, disposables) { const PaymentGateway_ = new PaymentGateway(); const OrdersRepository_ = new OrdersRepository(inputs.Database); const OrdersService_ = new OrdersService(OrdersRepository_, PaymentGateway_); const OrdersController_ = new OrdersController(OrdersService_); const OrderPlacedHandler_ = new OrderPlacedHandler(OrdersService_); return {
PaymentGateway: PaymentGateway_,
OrdersRepository: OrdersRepository_,
OrdersService: OrdersService_,
OrdersController: OrdersController_,
OrderPlacedHandler: OrderPlacedHandler_,
};
}Hover a class to trace it from source to emitted wiring.
inputs.Database comes from the imported module.
Wiring mistakes are build errors
A missing provider or an unsatisfied import is a named diagnostic on the exact module line — at compile time, not a 500 at 3 a.m.
Boot is just function calls
Construction order was solved at build time, so startup is executing a handful of new expressions — re-boot from the memoized app is measured under 220 ns.
Runs where reflection can’t
The output contains no eval and no runtime scanning, so the same app deploys to JIT-less edge runtimes like Cloudflare Workers unchanged.
Measured, not promised
A framework you can weigh.
0 KB
gzipped HTTP floor on Cloudflare Workers
~47 KB for the Node build
+0 ms
cold-start overhead on Workers
54.6 ms empty worker → 60.0 ms full app
up to 0k
requests/s on one core
40 modules · 400 providers · 440 routes
<0 ns
re-boot from the memoized app
boot wiring is precomputed at build time
Measured in-repo on the benchmark fixtures — read the baselines.
Fails loud
Broken wiring never boots.
Errors as you type
Module(...)'s generic typing runs entirely in your editor — a constructor dependency nothing visible provides is flagged on the exact providers entry, before any build runs.
Plain TypeScript, no plugin
It's just types: any editor with a TypeScript language server shows it, and tsc/tsgo in CI sees exactly the same error.
What it catches
Invisible constructor dependencies, exports nothing backs, an imported module's unmet requires, a missing eager: true, a misspelled config key.
1import { Module } from "@heximon/runtime";
2import { WelcomeEmailSender } from "./welcome-email.sender";
3
4export class NotificationsModule extends Module({
5 providers: [WelcomeEmailSender],
6}) {}Property '"ERROR — constructor dependency not provided in this module"' is missing in type 'typeof WelcomeEmailSender'.
The required type names the missing token: MailerService.
Real output — the message this repo's type-diagnostics test asserts against a live tsgo run.
Capabilities
Growth is one line at a time.
Worker bundle
~26.0 KBgzip
Events through Health are the measured ~14 KB gzip stack on the Workers build (per-package split approximate); Durable Objects, Cron, Sagas, and Workflows are in-repo estimates (~). Opt-in weight, not baseline weight.
import { defineHeximonConfig } from "@heximon/build";import { HttpPlugin } from "@heximon/http/compiler";export default defineHeximonConfig({ plugins: [ new HttpPlugin(), ],});Toggle a capability — the config is the whole registration step.
Contracts
Declare once. Typed everywhere.
import { Contract, Route } from "@heximon/contract";
export class OrdersApi extends Contract({
prefix: "/api/orders",
routes: { list: Route.get("/").responses({ 200: orderList }), create: Route.post("/")
.body(createOrder)
.responses({ 201: order }), },
}) {}Hover a route to follow it into every consumer.
export class OrdersController implements Controller<OrdersApi> { public async list(action: Action<OrdersApi, "list">) {
return action.json(await this.orders.list());
} public async create(action: Action<OrdersApi, "create">) {
// invalid body → 400 problem+json, before your code runs
const body = await action.request.readValidatedBody();
return action.respond(201, await this.orders.create(body));
}}const api = new OrdersApi(ClientTransport.fetch({ baseUrl }));
const created = await api.client.create({
body: { sku: "HEX-01", qty: 2 },
});// typed from the 201 schema — non-2xx throws ContractErrorTyped REST client
the Contract class is the client
Typed WS client
WsClient over a WebSocketContract
Typed SSE client
SseClient over a ServerSentEvents class
OpenAPI docs
generated from the same value
MCP server
tools 1:1 from the route table
Delete the create handler and the controller class fails to compile. Rename a field and every drifted call site turns red — the handler, the REST call, the WS send, the SSE subscription — you fix them in the editor, not in production.
Realtime
Realtime, compiled in.
One contract, both directions
clientToServer and serverToClient schema maps validate every frame both ways — an unknown kind or invalid payload never reaches your handler.
A method per message kind
Bind one handler method per inbound kind; the compiler enforces exhaustiveness at build time, so a new message kind is a build error until you handle it.
Hibernation-proof on the edge
On Cloudflare the connection rides a keyed Durable Object — path params and connection data survive a hibernation wake, byte for byte.
export class SeatMapContract extends WebSocketContract({
path: "/ws/events/:eventId/seats",
clientToServer: {
"seat.hold": SeatSelectionSchema,
"seat.release": SeatSelectionSchema,
},
serverToClient: {
"seat.map": SeatMapSchema,
"seat.update": SeatUpdateSchema,
},
}) {}const conn = new SeatMapContract(
WsTransport.native({ baseUrl: "wss://api.example.com" }),
).connect({ eventId: "evt-42" });
conn.on("seat.update", (data) => {
// data is typed: { seat, state: "held" | "released", … }
});
conn.send("seat.hold", { seat: 14 }); // validated before it leavesSame handler code, two hosts.
In-process broadcast on a Node server; keyed, hibernating Durable Objects on Cloudflare — switching is a deploy target, not a rewrite.
Deploy
One codebase. Eight native targets.
vp build emits each platform's native artifact. The compiled output has no eval and no JIT requirement, so it runs where reflection-based frameworks can't. Unsupported combinations fail loud — at build time. A feature the target platform can't run (a durable workflow on a platform without timers, a cron on a host with no scheduler) fails your build with a named diagnostic — never your production.
Deploy guideGet started