The vyuh Accessor
After VyuhServer.bootstrap(...) returns, the global vyuh is bound to the runtime. Handlers, middleware, and feature init callbacks reach the resolved infrastructure through it.
import 'package:vyuh_server/vyuh_server.dart';
vyuh.db; // DbAdapter — relational, OLTP
vyuh.storage; // StorageAdapter — file / blob storage
vyuh.auth; // AuthModule
vyuh.telemetry; // TelemetrySink
vyuh.query; // QueryCompiler? (null when no QueryPlugin)
vyuh.policy; // PolicyRegistry
vyuh.di; // DiContainer
vyuh.env; // EnvRegistry
vyuh.errors; // ErrorCodeRegistryWhy Globals?
The accessor is a deliberate choice. The alternatives — passing the runtime through every layer, or resolving via a service locator string — both add friction without adding safety. A typed global keeps handlers terse, gives every cross-cutting concern its own verb, and avoids the "where does this come from?" question.
The trade-off: a single isolate hosts one running server. Tests that boot and shut down servers in sequence work fine (runtime.shutdown() unbinds at the end). Tests that need two servers in the same isolate aren't supported in v0.1.
The Verbs
vyuh.db — DbAdapter
final rows = await vyuh.db.execute(
'SELECT * FROM products WHERE tenant_id = @tenant',
parameters: {'tenant': tenantId},
);
await vyuh.db.runTx((session) async {
await session.execute('INSERT INTO orders (...) VALUES (...)');
await session.execute('INSERT INTO order_items (...) VALUES (...)');
});
await for (final payload in vyuh.db.listen('order_events')) {
// Process Postgres NOTIFY payloads
}Throws StateError if no DbPlugin was registered.
vyuh.storage — StorageAdapter (file storage)
Bucket-and-path keyed object storage for avatars, document uploads, exported reports, generated PDFs. Distinct from vyuh.db (relational).
// Upload bytes
await vyuh.storage.upload(
bucket: 'avatars',
path: 'users/$userId.png',
bytes: pngBytes,
contentType: 'image/png',
upsert: true,
);
// Public CDN URL (only valid for public buckets) — sync, no IO
final cdnUrl = vyuh.storage.getPublicUrl(
bucket: 'public-assets',
path: 'logos/brand.svg',
);
// Short-lived signed URL for a private bucket
final signedUrl = await vyuh.storage.createSignedUrl(
bucket: 'reports',
path: 'q3/$tenantId-summary.pdf',
expiresIn: Duration(minutes: 5),
);
// List with prefix + page
final objects = await vyuh.storage.list(
bucket: 'avatars',
prefix: 'users/',
limit: 100,
);
// Move / copy / delete
await vyuh.storage.move(bucket: 'tmp', fromPath: 'a.bin', toPath: 'archive/a.bin');
await vyuh.storage.copy(bucket: 'reports', fromPath: 'live.pdf', toPath: 'history/$ts.pdf');
await vyuh.storage.delete(bucket: 'avatars', paths: ['users/$userId.png']);Throws StateError if no StoragePlugin was registered — apps that don't need file storage simply don't reach for this accessor.
vyuh.auth — AuthModule
// Inside a handler on a protected path:
final actor = req.actor;
actor.id; // user id / service id
actor.tenantId; // tenant id (from JWT claim or API-key lookup)
actor.roles; // List<String>
actor.claims; // raw claims map
// Walk the strategy chain manually (rarely needed):
final result = await vyuh.auth.resolve(AuthContext.fromRequest(req));If no AuthPlugin is registered, the module has zero strategies and falls back to AnonymousActor.
vyuh.telemetry — TelemetrySink
Future<Response> listOrders(Request req) async {
final span = vyuh.telemetry.startSpan(
'orders.list',
attrs: {'tenant_id': req.actor.tenantId},
);
try {
final rows = await loadOrders();
span.setAttribute('order_count', rows.length);
vyuh.telemetry.counter('orders.listed', delta: rows.length);
return jsonResponse(body: {'success': true, 'data': rows.asMaps});
} catch (e, st) {
span.recordException(e, st);
rethrow;
} finally {
span.end();
}
}With no TelemetryPlugin registered, every call is a no-op — handlers don't need to guard vyuh.telemetry.* behind null checks.
vyuh.query — QueryCompiler?
final queryJson = req.url.queryParameters['query'];
if (queryJson != null && vyuh.query != null) {
final compiled = vyuh.query!.compile(jsonDecode(queryJson));
// compiled.whereClause + compiled.parameters
}Returns null when no QueryPlugin is registered — handlers should null-check (or register PostgresQueryPlugin and ignore the question).
vyuh.policy — PolicyRegistry
final decision = await vyuh.policy.evaluate(
'order.cancel',
PolicyContext(
actor: req.actor,
attrs: {'order_id': orderId},
),
);
switch (decision) {
case Allowed():
// proceed
case Denied(:final error):
throw error; // bubbles to the error handler
}Unregistered refs return Allowed by default (permissive). Domains that need deny-by-default wrap the registry with their own helper.
vyuh.di — DiContainer
final renderer = vyuh.di.get<ReportRenderer>();
final s3 = vyuh.di.get<S3Client>();The container is populated by every plugin's and feature's di: slot during bootstrap. Per-request scoping is available through req.di, which parents to vyuh.di.
Reserve the DI container for the long tail. Cross-cutting infrastructure already has typed verbs (vyuh.db, vyuh.auth, …) — don't double- register them through DI.
vyuh.env — EnvRegistry
final db = vyuh.env.get<DatabaseConfig>();
final stripe = vyuh.env.get<StripeConfig>();Throws StateError for missing types — treat the missing binding as a bootstrap bug, not a runtime branch.
Bind types by passing TypedEnvConfig.factory<T>(...) to EnvPlugin(configs: [...]) at bootstrap. The plugin populates the registry in its init() hook — before any other plugin's init() — so cross-plugin reads from vyuh.env.get<T>() are answerable everywhere.
vyuh.errors — ErrorCodeRegistry
Used internally by GlobalMiddleware.errorHandler(). Most apps don't talk to it directly — they thrown StructuredException subclasses and let the middleware resolve the wire format.
Inside Init Callbacks
The global is bound before plugin init runs, so init callbacks can call vyuh.* for other plugins:
class WarmupPlugin extends Plugin {
@override
final String name = 'app.warmup';
@override
List<String> get dependencies => ['kernel.postgres'];
@override
Future<void> init(Object runtime) async {
// Postgres plugin already initialized (dependency).
await vyuh.db.execute('SELECT 1'); // smoke test
await loadCacheFromDb();
}
}runtime vs vyuh
The runtime is passed to init(runtime) and dispose(runtime). The global vyuh is bound to the same runtime. They are interchangeable — pick whichever reads better:
// Equivalent inside init:
await runtime.storage.execute('SELECT 1');
await vyuh.db.execute('SELECT 1');Outside of init / handlers, prefer vyuh — it's terse and the implication that "there is one running server in this isolate" is already true.
Tests
In tests, bootstrap a VyuhServer, exercise the handlers, then shut down:
test('catalog API returns products', () async {
final runtime = await VyuhServer.bootstrap(
name: 'catalog-test',
plugins: [
InMemoryStoragePlugin(),
ConsoleTelemetryPlugin(write: testLog.add),
],
features: [catalogFeature],
);
try {
final res = await runtime.start(port: 0).then(...);
// ...
} finally {
await runtime.shutdown();
}
});shutdown() calls vyuh.unbind() at the end so the next test's bootstrap starts fresh.
Where to Go Next
- Storage — Postgres — what
vyuh.dbactually is - Auth — JWT — wiring an auth chain
- Telemetry — OTel — production spans and metrics