Design Philosophy
Vyuh Server is built on a small set of opinions. They drive every concrete decision in the framework, from how plugins look to how route mounting is ordered.
1. Plugins and Features Speak the Same Language
A plugin is a singleton role: one storage adapter, one telemetry sink, one OpenAPI generator. A feature is a domain area: catalog, billing, reports. They are different identities — but they should not be different to learn.
Vyuh Server makes them structurally identical. Both contribute through the same five slots:
| Slot | Purpose |
|---|---|
routes | Declarative method/path/handler tuples |
middlewares | Path-scoped middleware with explicit order |
context | Per-request typed value providers |
di | Server-wide DI bindings on vyuh.di |
descriptors | Open extension — capability shapes as data |
extend(scope) | Imperative escape hatch over the live RouterScope |
Anyone fluent in one is fluent in the other. There's no separate "plugin SDK" versus "feature API" — there's just one contribution model.
2. Data Over Code
Wherever a capability can be expressed as data, it is. Features describe entities as EntityCrudDescriptor<T>, error codes as ErrorCodesDescriptor. A plugin that mixes in DescriptorHandler<D> claims the type at boot and emits the implementation.
Why this matters:
- Composition without coupling. Features don't know which plugin will handle their descriptors. Swap the entity-CRUD implementation for a caching variant — features don't change.
- Fail-fast at boot. Unhandled descriptors throw at startup with a clear message ("no plugin claimed
EntityCrudDescriptor<Order>"). No silent capability loss at runtime. - Inspectability. Features expose their capabilities as readable data structures, not opaque registration calls.
3. Typed Verbs Over Stringly-Typed Lookups
The vyuh global accessor publishes typed verbs:
vyuh.db.execute('SELECT …');
req.actor;
vyuh.telemetry.startSpan('handler');
vyuh.policy.evaluate(ref, ctx);
vyuh.env.get<DatabaseConfig>();No raw string env reads in handlers, no serviceLocator<MyService>(), no scope.read('storage'). Cross-cutting infrastructure has first-class verbs; the DI container is reserved for the long tail (domain singletons, test fakes, per-deployment configs).
4. Singletons Fail Fast on Collision
A DbPlugin, TelemetryPlugin, QueryPlugin, EnvPlugin, and an auth fallback are each at-most-one. Registering two throws SingletonCollisionError at bootstrap with both plugin names — no last-wins silently overriding the first.
The intent is to make environment-based plugin selection explicit:
plugins: [
PostgresDbPlugin(),
if (isProd)
OtelTelemetryPlugin(defaultServiceName: 'saas-api')
else
ConsoleTelemetryPlugin(),
],5. Onion Middleware with Explicit Order
Every middleware declares a numeric order. Larger order = outermost. Plugins and features contribute into the same merged stack, sorted by order. Ties break by declaration order, with plugins first (infrastructure-level).
The framework's own middleware lives at known points:
| Layer | Where |
|---|---|
| Client context (request id, IP, UA) | Outermost (mounted last) |
| User-supplied global middleware | Below client context |
MiddlewareSpecs from plugins + features | Sorted by order |
| Auth on protected paths | Just outside the handler |
| Per-request context providers | Closest to the handler |
| Route-local middleware | Innermost |
| Handler | The center of the onion |
No surprises. No hidden ordering rules. Every layer is in serve.dart for anyone to read.
6. Graceful by Default
runtime.start() wires SIGINT + SIGTERM by default. On the first received signal:
- Every feature's
disposeruns in reverse-topological order. - Every plugin's
disposeruns in reverse order. - Your optional
onShutdowncallback runs. - The HTTP server closes.
exit(0).
Repeated signals during shutdown are ignored — no double-dispose, no half-finished tear-down. Pass gracefulSignals: const [] to opt out (tests or hosts that manage lifecycle themselves).
7. Pure Dart
No JVM, no Node, no Python. Vyuh Server runs anywhere Dart runs — Docker on your laptop, ECS Fargate, Cloud Run, a Raspberry Pi. It compiles to a single native binary with dart compile exe, starts in milliseconds, and holds a small memory footprint.
It's also the same language as your Flutter clients — share entity types, validation, and error definitions through pure-Dart packages with no JSON schema duplication.
What This Is Not
- Not a framework that hides HTTP. Routes are Relic
Handlers. You readreq.headers, writeResponse.ok(...), and streamBodys directly when you need to. - Not a magic ORM. The
DbAdapteris a thin transactional SQL boundary. Use it for raw queries; reach for an ORM at the application level if you want one. - Not opinionated about your auth provider. JWT, API key, mTLS, session cookie, custom — every strategy is a callback you write.