Diagram
100%

Drag to pan · Scroll to zoom · + / − to zoom · 0 to fit · Esc to close

Skip to content

6. Modules and Assembly ​

A module is a release and governance boundary. A Blueprint is a selected set of modules. Assembly produces the effective graph consumed by generators and runtimes.

Declare bounded modules ​

dart
const directoryRef = ModuleRef('directory');
const opsRef = ModuleRef('ops');
const companyRef = EntityRef(module: directoryRef, name: 'company');
const siteRef = EntityRef(module: directoryRef, name: 'site');
const userRef = EntityRef(module: directoryRef, name: 'user');

final directoryModule = Module(
  name: 'directory',
  title: 'Directory',
  version: '1.0.0',
  entities: [companyEntity, siteEntity, userEntity],
  exports: const [
    EntityExport(entity: companyRef),
    EntityExport(entity: siteRef),
    EntityExport(entity: userRef),
  ],
);

final opsModule = Module(
  name: 'ops',
  title: 'Operations',
  version: '1.0.0',
  entities: [areaEntity, equipmentEntity],
  dependencies: const [directoryRef],
);

final blueprint = Blueprint(
  name: 'pharma_portfolio',
  version: '1.0.0',
  modules: [directoryModule, opsModule],
);

One Postgres schema per module is the normal posture. Modules can release at different versions even though one Blueprint revision selects them together.

Cross-module access is explicit ​

For ops.area.site_id to target directory.site:

  1. Directory exports site.
  2. Ops lists directory in dependencies.
  3. The reference/relationship targets the stable type directory.site.
  4. The dependency graph remains acyclic.

Do not bypass the boundary with an unchecked string or an application-only join.

Exports are typed by intent:

dart
exports: const [
  EntityExport(entity: userRef),
  DerivedValueExport(
    DerivedValueRef(
      facet: FacetRef(entity: userRef, name: 'identity'),
      name: 'is_active',
    ),
  ),
  ActionExport(
    ActionRef(
      facet: FacetRef(entity: userRef, name: 'lifecycle'),
      name: 'deactivate',
    ),
  ),
]

Contribute facets without redefining an entity ​

An Ops package may add a qualification facet to directory.user:

dart
final userOpsDescriptor = EntityDescriptor(
  target: 'directory.user',
  aspect: 'ops',
  title: 'Operations qualification',
  priority: 100,
  facets: const [
    Facet(
      name: 'ops_qualification',
      fields: [
        BooleanField(
          'gmp_qualified',
          title: 'GMP Qualified',
        ),
      ],
      ui: FacetUI(
        kind: UISectionKind.detail,
        order: 300,
      ),
    ),
  ],
);

final opsModule = Module(
  // ...
  dependencies: const [directoryRef],
  descriptors: [userOpsDescriptor],
);

EntityDescriptor layers facets, aggregates, and entity UI posture onto the same stable entity type. It does not mint a competing ops.user.

dart
final findings = BlueprintValidator.validate(blueprint);
if (findings.isNotEmpty) {
  throw StateError(findings.join('\n'));
}

final EffectiveBlueprint effective = blueprint.assemble();
final EffectiveEntity? user = effective.entity('directory.user');

print(user?.entity.facets.map((facet) => facet.name));
print(user?.originGraph);

Assembly:

  • orders descriptor contributions by priority and declaration order;
  • merges facets, aggregates, and UI descriptors;
  • creates the effective action catalog;
  • retains an origin graph explaining where every contribution came from.

Generators should consume the effective model, normally by calling APIs that assemble internally.

One Blueprint owns domain and application surfaces ​

Blueprint declares domain semantics and application surfaces together. BlueprintDescriptorSet is an internal contribution mechanism that assembles back into that one root:

dart
final set = BlueprintDescriptorSet([
  directoryDescriptor,
  opsDescriptor,
]);

final blueprint = set.blueprint(
  name: 'ops',
  title: 'Operations',
  version: '1.0.0',
);

The declaration remains usable by a server with no Flutter dependency. Its application section adds routes, collections, editors, dashboards, shell, and client extension references without introducing another Blueprint type.

Label production releases and lower environments differently ​

The app Blueprint can declare environment-aware header identity without hard-coding deployment rules into a Flutter header:

dart
const deployment = DeploymentUI(
  enabled: true,
  productionHeaderLabels: [
    DeploymentValue.releaseVersion,
  ],
  nonProductionHeaderLabels: [
    DeploymentValue.environment,
  ],
  windowTitleLabels: [
    DeploymentValue.application,
    DeploymentValue.organization,
    DeploymentValue.environment,
  ],
);

The host supplies the active DeploymentContext. The runtime then places the effective label beside the app identity:

  • production shows the released version, such as 2.4.1, and omits the redundant Production environment badge by default;
  • development, test, UAT, training, validation, staging, and sandbox show their environment label instead of presenting a lower build as a production release;
  • headerLabels remains the fallback when an environment-specific list is not declared.

This information is display and cache-partition metadata. The server still derives tenant authority, entitlements, and permissions from the authenticated request.

Checkpoint ​

Create three modules:

  • directory: company, site, user;
  • access: role, permission, assignment; depends on Directory;
  • ops: area and equipment; depends on Directory and Access.

Export only the entities and triggers another module is allowed to consume. Run validation and inspect the effective origin graph.

Next: Database, security, and seed hints.

References: Entity descriptors · Developer guide.