Skip to content

Design Philosophy ​

The Vyuh Entity System rests on six principles. Every API decision falls out of one of them.

1. Configuration over Code ​

Instead of writing boilerplate CRUD scaffolding for each entity, you describe the entity once through EntityConfiguration<T> and let the framework do the plumbing.

dart
final courseConfig = EntityConfiguration<Course>(
  metadata: EntityMetadata(
    identifier: 'courses',
    name: 'Course',
    pluralName: 'Courses',
    icon: FluentIcons.book_24_regular,
    visibility: UIVisibility.all(menuCategory: lmsMenuCategory),
  ),
  api: const CourseApi(), // extends HttpEntityApi<Course>
  routing: EntityRouting<Course>(
    path: NavigationPathBuilder.collection(prefix: '/lms/courses'),
    builder: StandardRouteBuilder<Course>(),
    mode: EntitySelectionMode.responsive,
  ),
  layouts: EntityLayouts<Course>(list: [courseTableLayout]),
  actions: EntityActions<Course>(
    inline: StandardEntityActions.inline<Course>(),
    header: StandardEntityActions.header<Course>(),
  ),
);

That single descriptor generates routes, navigation entries, authorization checks, and UI scaffolding. Adding a new entity type is one more configuration, not hundreds of lines of plumbing.

When you opt into the generator, even this descriptor is emitted from @Entity annotations on the model — see the Annotations guide.

2. Type Safety ​

The entity system leans on Dart's type system. Generics ensure that configurations, APIs, layouts, editors, and actions are all bound to the correct entity type at compile time.

dart
EntityConfiguration<Course>(
  api: const CourseApi(),               // must be EntityApi<Course>
  layouts: EntityLayouts<Course>(...),  // CollectionLayout<Course>, EntityLayout<Course>
  actions: EntityActions<Course>(...),  // EntityAction<Course>, CollectionAction<Course>
  routing: EntityRouting<Course>(...),  // NavigationRouteBuilder<Course>
);

Field definitions are also type-bound:

dart
// TextFieldDef<Course> can only be used in columns/forms for Course.
static final title = TextFieldDef<Course>(field: 'name', label: 'Title');

This catches "wrong entity type passed to a layout" or "wrong field referenced in a column" at compile time, before they become runtime errors.

3. Separation of Concerns ​

The system is split across focused packages, each owning one concern:

PackageResponsibility
vyuh_entity_annotations@Entity / @Field / Authorize — pure Dart, no Flutter
vyuh_entity_systemModels, API, services, authorization, plugin (widget-free)
vyuh_entity_system_uiLayouts, editors, route builders, command palette (Flutter)
vyuh_entity_generatorbuild_runner builder that emits *.entity.dart files

Within each package, concerns are further separated. EntityApi<T> handles data access, EntityEditor<T> handles form logic, EntityLayouts<T> handles presentation, and EntityRouting<T> handles navigation. They are composed inside EntityConfiguration<T> but stay independently configurable and testable.

4. Plugin Architecture ​

The entity system integrates with the Vyuh Framework as a plugin. EntitySystemPlugin owns:

  • Entity registration and lookup (getConfig<T>(), getConfigByIdentifier)
  • Route generation for every registered entity
  • The shared service container (vyuh.entity?.services — query cache, realtime, name cache, error display, search sync, etc.)
  • The pluggable AuthorizationProvider

Entities are registered per feature via EntityExtensionDescriptor:

dart
FeatureDescriptor(
  name: 'lms',
  extensions: [
    EntityExtensionDescriptor(
      entities: [courseConfig, trainerConfig],
      services: [
        EntityServiceRegistration<EnrollmentCountService>(
          EnrollmentCountService(),
        ),
      ],
      fieldFormatters: [
        FieldFormat(field: 'completed_at', type: cdx.FieldType.dateTime),
      ],
    ),
  ],
);

Each feature module owns its registrations, keeping the application modular and independently deployable.

5. Responsive by Default ​

Entity navigation adapts to screen size through EntitySelectionMode:

ModeBehavior
noneNo navigation on selection — only fires the callback
navigateAlways navigate to the detail page (default)
responsiveNavigate on mobile, side panel on desktop

Layouts are responsive too. EntityLayouts<T>.list can hold multiple collection layouts (a EntityTableConfig, a EntityGridConfig, custom ones) and the UI surfaces a layout switcher when more than one is configured. Detail layouts collapse to a tab strip on smaller viewports.

The newer EntityWorkspace<T> shell composes routing + layouts + side panes into a single workspace surface — see the Architecture overview for how the pieces snap together.

6. Authorization-Aware ​

Authorization is woven through the system, not bolted on at the end. Every gate uses the same declarative Authorize DSL from vyuh_entity_annotations:

dart
Authorize.permission('lms.courses.update')
Authorize.anyPermission(['lms.courses.publish', 'lms.courses.update'])
Authorize.all([
  Authorize.permission('lms.courses.update'),
  Authorize.role('lms_admin'),
])

It shows up in four places:

  • Routes — EntityRoutePermissions on EntityRouting<T> gates list, create, view, edit, and dashboard routes.
  • Actions — EntityAction<T>.authorize and CollectionAction<T>.authorize hide the action when the expression is not satisfied.
  • Layouts — EntityLayouts<T>.filterByPermission(...) filters detail tabs and list layouts the user is not allowed to see.
  • UI gates — AuthorizeGate and AuthorizeGuard widgets in vyuh_entity_system_ui wrap any custom Flutter subtree with the same DSL.

The AuthorizationProvider interface is pluggable:

dart
// Open provider for development — every Authorize check passes.
EntitySystemPlugin(
  baseUrl: 'https://api.example.com',
  authorizationProvider: OpenAuthorizationProvider(),
);

// Real provider for production.
EntitySystemPlugin(
  baseUrl: 'https://api.example.com',
  authorizationProvider: MyAuthorizationProvider(httpClient),
);

The provider is async-loaded once (provider.whenReady) and then queried synchronously from UI code via provider.can(expression). Permission and role decisions are cached for the lifetime of the session — call AuthorizationProvider.refresh() (or recreate the provider) on logout.

Design Tradeoffs ​

TradeoffRationale
Upfront configuration costPays off as entity count grows — each new entity is one descriptor, not a feature rewrite
Generic type complexityCatches errors at compile time instead of runtime
Two-package split (runtime / UI)Adds a dependency boundary but enables pure-Dart testing and server-side reuse
Plugin registration ceremonyIntegrates cleanly with Vyuh's modular feature system

Next Steps ​