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.
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.
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:
// 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:
| Package | Responsibility |
|---|---|
vyuh_entity_annotations | @Entity / @Field / Authorize — pure Dart, no Flutter |
vyuh_entity_system | Models, API, services, authorization, plugin (widget-free) |
vyuh_entity_system_ui | Layouts, editors, route builders, command palette (Flutter) |
vyuh_entity_generator | build_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:
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:
| Mode | Behavior |
|---|---|
none | No navigation on selection — only fires the callback |
navigate | Always navigate to the detail page (default) |
responsive | Navigate 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:
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 —
EntityRoutePermissionsonEntityRouting<T>gates list, create, view, edit, and dashboard routes. - Actions —
EntityAction<T>.authorizeandCollectionAction<T>.authorizehide 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 —
AuthorizeGateandAuthorizeGuardwidgets invyuh_entity_system_uiwrap any custom Flutter subtree with the same DSL.
The AuthorizationProvider interface is pluggable:
// 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
| Tradeoff | Rationale |
|---|---|
| Upfront configuration cost | Pays off as entity count grows — each new entity is one descriptor, not a feature rewrite |
| Generic type complexity | Catches 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 ceremony | Integrates cleanly with Vyuh's modular feature system |
Next Steps
- Architecture — how the packages and layers are organized
- Entity Model —
EntityBase, mixins, and the model hierarchy - Configuration — full
EntityConfigurationreference - Permissions concept page —
AuthorizeDSL deep dive