Skip to content

Architecture

The entity system is a Vyuh plugin that turns a typed entity declaration into everything an enterprise screen needs: a typed CRUD API, reactive caches, declarative routes, layouts, editors, authorization gates, and a workspace shell. This page explains the package layering, the pieces a plugin owns, and the lifecycle that wires them together.

Package layers, one direction

PackageContentsFlutter dependency
vyuh_entity_annotationsPure-Dart annotations (@Entity, @Field, @ServerEntity), field companion declarations, UI layout declarations, relationship declarations, and the Authorize DSL.None.
vyuh_entity_generatorbuild_runner builders that emit client *.entity.dart files and server *.server.dart files from annotation metadata.None at runtime; dev dependency only.
vyuh_entity_systemEntityBase + mixins, EntityApi / HttpEntityApi, EntityConfiguration, EntityActions, layout descriptors (data-only), services container, drafts and versioning models, the AuthorizationProvider interface, plugin registration.Minimal — only IconData.
vyuh_entity_system_uiWorkspace shapes, route builder, list controller, default tab/version/audit layouts, editor widgets, error/import/export dialogs, formatting helpers.Full Flutter widgets.

The UI package depends on the core package; the core package never imports the UI package. Annotation metadata is shared through vyuh_entity_annotations. Regulated entity/action contracts live with the entity blueprint packages, while concrete IAM, policy, audit, and messaging adapters compose around the entity runtime.

Layered view at runtime

LayerResponsibility
ApplicationDeclares features and entity configurations.
PluginRegisters entities, generates routes, owns the services container, holds the AuthorizationProvider.
Core servicesCaches reads, brokers cache invalidations, resolves names, polls realtime, drives search indexing.
UIHosts workspaces, drives selection ↔ URL ↔ panel state, renders layouts, runs editors.

EntitySystemPlugin lifecycle

EntitySystemPlugin implements the standard Vyuh Plugin interface. It holds:

  • baseUrl — the API host all HttpEntityApis resolve against.
  • authorizationProvider — synchronous resolver for Authorize expressions. Defaults to OpenAuthorizationProvider (allow-everything) so a fresh app boots without authorization wiring.
  • signatureProvider — optional, enables SignatureDrivenEditor.
  • filterWidgetDelegate — optional, supplies the filter UI.

Authorization wiring

The plugin treats authorization as a first-class concern. On every emission from vyuh.auth.userChanges it calls authorizationProvider.refresh(), which re-hydrates the actor's permissions, roles, and user-group memberships. The provider exposes synchronous reads (hasPermission, hasRole, isMemberOf, can(Authorize)) so layouts and actions can gate themselves without futures. See Permissions for the expression model.

Core services

ServicePurpose
EntityLocalizationServiceResolves locale-aware entity / layout / category names.
EntityNameCacheServiceIn-memory cache of (entityType, id) → name for FK rendering.
EntityHelpServicePer-entity contextual help bundles.
SearchSyncServiceBackground sync of searchable entity records.
EntityRealtimeServicePolling-based change detection.
QueryCacheServiceStale-while-revalidate cache + mutation announcement bus.
SignatureVerificationServiceE-signature verification (only when signatureProvider is set).

Services are registered through EntityServicesContainer keyed by their runtime Type. Anyone can fetch them via vyuh.entity?.services.get<T>() or tryGet<T>().

Registering an entity

Entities are registered via an EntityExtensionDescriptor inside a feature:

dart
final lmsFeature = FeatureDescriptor(
  name: 'lms',
  title: 'Learning Management System',
  extensions: [
    EntityExtensionDescriptor(
      entities: [courseConfig, trainerConfig, certificationConfig],
      services: [EntityServiceRegistration(MyDomainService())],
      fieldFormatters: [
        FieldFormat(field: 'instructor_id', formatter: trainerFormatter),
      ],
      hierarchies: [areaHierarchy],
    ),
  ],
);

The descriptor surfaces four kinds of contribution:

  • Entities — each EntityConfiguration<T> is registered with the plugin.
  • Services — domain services that need lifecycle management.
  • Field formatters — declarative FieldFormat rules that apply across the app, matched by literal field name or regex pattern.
  • Hierarchies — composite tree definitions used by hierarchy widgets.

Once registered, calling plugin.generateRoutes() walks every entity whose metadata.visibility includes a RouteVisibility (or MenuVisibility) and delegates route construction to its routing.builder.

From config to URLs

StandardRouteBuilder<T> (in the UI package) turns one EntityConfiguration<T> into a tree of GoRoutes organised by slot: createRoute, editRoute, dashboardRoute, viewTabsRoute, customRoutesSlot, singletonMainRoute, singletonEditRoute, singletonTabsRoute. Subclasses override buildSingleRoute / buildMultipleRoutes and dispatch on the slot name to substitute one route without rebuilding the whole tree.

URLs are never hand-constructed by callers. EntityConfiguration.route (alias for routing.path) returns a NavigationPathBuilder with helpers for list, view, edit, viewTab, create, dashboard, and custom. Two factory constructors cover the two patterns:

FactoryUse caseURL shape
NavigationPathBuilder.collection(prefix:)Multi-instance entities/prefix, /prefix/:id, /prefix/:id/edit
NavigationPathBuilder.singleton(prefix:)Single-instance entities/prefix, /prefix/edit (id ignored)

See the API reference for the full method surface.

Workspace shapes

Routes are mounted inside a workspace. Workspaces are StatefulWidgets that own URL ↔ selection ↔ panel synchronisation, controller lifecycles, and mutation event dispatch. They compose by mixin so a new shape only adds the capabilities it needs.

WorkspaceRole
EntityWorkspace<T>Standard list-with-detail. Owns an EntityListController<T> and a cdx_panes workspace.
SingletonEntityWorkspace<T>Singleton entity host — no list, no row selection, just the active route's content.
GroupedEntityWorkspaceTabbed shell hosting child workspaces (used by Inbox-style screens).

All three extend the shared WorkspaceTemplate base, which standardises:

  • A single GoRouter listener for URL change reactions.
  • A central disposers list so capability mixins can register teardown.
  • A QueryCacheService.invalidations subscription that fans out typed MutationEvents to the workspace's onAfterMutation callback.

See Layouts for how layout slots compose with each shape.

Where each subsystem lives

ConcernPackageKey types
Annotation metadataannotationsEntity, Field, ServerEntity, FieldCompanion, Authorize
Code generationgeneratorentityBuilder, serverEntityBuilder
Regulated runtime contractsruntime_coreAccessExpression, PolicyResolver, OperationEnvelope, EvidenceEnvelope, OutboxMessage
Identity & auditcoreEntityBase, Auditable, Versionable, Draftable
CRUDcoreEntityApi, HttpEntityApi, EndpointBuilder
CachingcoreQueryCacheService, Mutation, CachingPolicy
AuthorizationcoreAuthorizationProvider, Authorize (annotations)
ConfigurationcoreEntityConfiguration, EntityRouting, EntityActions, EntityLayouts
FormscoreEntityEditor, StandardEntityEditor, SignatureDrivenEditor, editor parts
Drafts & versioningcoreDraftMetadata, EntityVersion, EntityAudit
WorkspacesUIEntityWorkspace, SingletonEntityWorkspace, GroupedEntityWorkspace
RoutesUIStandardRouteBuilder, EntityCustomRoute, NavigationRouteBuilder
Reactive listsUIEntityListController, EntityListView, EntityView
Default layoutsUIversion / audit / item-table / delta-table layouts

Next steps