Skip to content

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:

SlotPurpose
routesDeclarative method/path/handler tuples
middlewaresPath-scoped middleware with explicit order
contextPer-request typed value providers
diServer-wide DI bindings on vyuh.di
descriptorsOpen 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:

dart
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:

dart
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:

LayerWhere
Client context (request id, IP, UA)Outermost (mounted last)
User-supplied global middlewareBelow client context
MiddlewareSpecs from plugins + featuresSorted by order
Auth on protected pathsJust outside the handler
Per-request context providersClosest to the handler
Route-local middlewareInnermost
HandlerThe 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:

  1. Every feature's dispose runs in reverse-topological order.
  2. Every plugin's dispose runs in reverse order.
  3. Your optional onShutdown callback runs.
  4. The HTTP server closes.
  5. 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 read req.headers, write Response.ok(...), and stream Bodys directly when you need to.
  • Not a magic ORM. The DbAdapter is 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.