Capabilities
Your app already has a module, a controller, a database call. Production adds a short, predictable list of concerns on top — none of them change how you got here. Every capability in this section is the same move:
add a package, add one config line — a plugin in heximon.config.ts and a namespace key in your
module's config — then write a class. The compiler discovers the class from that namespace key, not from
scanning your codebase, so nothing you write is "magically found" — it's listed, exactly once, where you'd
expect to look.
The one move, before and after
Start from the shape heximon create scaffolds — one plugin, nothing else:
import { defineHeximonConfig } from "@heximon/build";
import { HttpPlugin } from "@heximon/http/compiler";
// The capability manifest: one compiler plugin per capability. The entire HTTP layer is the one plugin
// below; everything you add later (events, CQRS, queues, …) is one more entry in the same list.
export default defineHeximonConfig({ plugins: [new HttpPlugin()] });
Add events — a second package, a second plugin, nothing else touched:
import { defineHeximonConfig } from "@heximon/build";
import { EventEmitterPlugin } from "@heximon/events/compiler";
import { HttpPlugin } from "@heximon/http/compiler";
export default defineHeximonConfig({
plugins: [new HttpPlugin(), new EventEmitterPlugin()],
});
Your module's config then lists the class under the plugin's namespace — one key, e.g.
events: { handlers: [OrderPlacedHandler] } — and you write the handler as a plain class. That's the whole
pattern; every row below is this same move with a different package and namespace key.
Not every capability needs a plugin at all — several (caching, key/value, blob, notifications, idempotency)
are plain injectable providers: you pnpm add the package and list a class in providers, no compiler
plugin and no namespace key required. The table below says which is which.
Every capability, one line each
| Capability | Package | The one config line | Page |
|---|---|---|---|
| Events | @heximon/events | plugins: [new EventEmitterPlugin()], module: events: { handlers } | Events |
| Background jobs (queue) | @heximon/queue | plugins: [new QueuePlugin()], module: queue: { handlers } | Queue |
| Scheduled work (cron) | @heximon/schedule | plugins: [new SchedulePlugin()], module: schedule: { handlers } | Scheduled Work |
| Caching | @heximon/cache | provider only: providers: [{ provide: Cache, useFactory: ... }], http: { middlewares: [CacheMiddleware] } / cqrs: { queryInterceptors: [QueryCacheInterceptor] } | Caching |
| Key/value storage | @heximon/kv | provider only: providers: [{ provide: Storage, useFactory: ... }] | Key/Value Storage |
| Blob storage | @heximon/blob | provider only: providers: [{ provide: BlobStore, useFactory: ... }] | Blob Storage |
| Health & readiness | @heximon/health | plugins: [new HealthPlugin()], module: health: { indicators } | Health & Readiness |
| Notifications | @heximon/notifications | provider only: providers: [EmailTransport-satisfying class] | Notifications |
| Idempotency | @heximon/events / @heximon/cqrs / @heximon/queue (./idempotency subpath) | provider only: list the interceptor directly under the existing events / cqrs / queue namespace, e.g. events: { interceptors: [EventIdempotencyInterceptor] } | Idempotency |
| Security hardening | @heximon/http (./security subpath) | provider only: providers: [RateLimitMiddleware-satisfying class], applied per-route via middlewares | Security Hardening |
| Observability & tracing | @heximon/otel | plugins: [new OtelPlugin()], top-level otel: {...} config key | Observability & Tracing |
| OpenAPI | @heximon/openapi | no plugin — construct new OpenApiGenerator(...) from your own controller | OpenAPI |
| MCP server | @heximon/mcp | no plugin — construct new McpServerGenerator(...) from your own controller, mark routes with .mcp({}) | MCP Server |
| Schema DTOs | @heximon/schema | no plugin — class X extends SchemaObject({...}) used directly in a route's body/responses | Schema DTOs |
| Branded IDs | @heximon/primitives | no plugin — type UserId = Branded<string, "UserId">, zero runtime cost | Branded IDs |
Production concerns, not architecture
Everything above makes an app that already works operationally sound — durable retries, observable, cache-fast, rate-limited. None of it asks you to restructure how you model your domain.
When your domain itself starts pushing back — multiple features converging on the same invariant, a process that needs a deadline, consistency that has to cross a service boundary — that's a different tier of problem, and Architecture is the gate that tells you which piece to reach for.
Project Structure
The recommended directory layout for a Heximon app — one folder per feature module, the three config files, naming conventions, and the generated boot entry.
Events
Decouple features with events across three tiers — the in-process EventBus, EventHandler and NotificationHandler, and domain events on aggregates with DomainEvents.emitFrom.