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:

  • RoutesEntityRoutePermissions on EntityRouting<T> gates list, create, view, edit, and dashboard routes.
  • ActionsEntityAction<T>.authorize and CollectionAction<T>.authorize hide the action when the expression is not satisfied.
  • LayoutsEntityLayouts<T>.filterByPermission(...) filters detail tabs and list layouts the user is not allowed to see.
  • UI gatesAuthorizeGate 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