The Domain Data Model
The persisted representation of a cloud environment (AWS, GCP, or Azure) lives in
src/aws/model.ts. It is deliberately decoupled from rendering: a resource
references a ServiceDefinition by id and stores config keyed by that service’s
ConfigField keys. This is exactly what the server stores and what the discovery
transform and IaC importers produce.
Entities
Account— an account in scope:accountId(an AWS 12-digit account id, or a GCP project / Azure subscription identifier),name, optionalenvironment("prod" | "staging" | …) andcolor.RegionRef—{ code, name }. The common set lives insrc/aws/regions.ts(REGIONS,regionName(code)); extend freely.ResourceInstance— a concrete instance of a service:serviceIdreferencesServiceDefinition.id.- Placement / scoping:
accountId,region, andparentId. config: Record<string, unknown>keyed by the service’sConfigFieldkeys.tags,arn(real ARN when known), andsource("manual" | "imported" | "mcp"— drives trust/edit affordances).position?: CanvasPosition(x, y, w, h) — presentation kept separate from data.raw?: RawSource— the verbatim sourcetype+propertiesfrom an IaC import, kept so the resource can be re-emitted faithfully (never holds credentials). See IaC Import & Export.
Relationship— a typed, directional (or symmetric) edge:{ id, from, to, kind, label?, source? }wherefrom/toareResourceInstance.ids andkindis aRelationshipKind.InfrastructureGraph— the top-level persisted entity:id,name,accounts[],resources[],relationships[], optionalviewport,createdAt/updatedAt(stamped by the repository, never by scripts),schemaVersion(SCHEMA_VERSION = 1, for forward migration), and optionaliacSource— the template-level sections (Parameters / Mappings / Conditions / Outputs / Metadata) preserved from an IaC import for faithful re-emit (see IaC Import & Export).
export interface InfrastructureGraph {
id: string;
name: string;
description?: string;
accounts: Account[];
resources: ResourceInstance[];
relationships: Relationship[];
viewport?: Viewport;
iacSource?: IacSource; // template-level sections kept for lossless IaC re-emit
createdAt?: string;
updatedAt?: string;
schemaVersion: number;
}Containment via parentId
Logical containment is modeled as a single field, ResourceInstance.parentId: a
VPC contains subnets, a subnet contains EC2 instances, etc. This is a tree
reference, distinct from the relationship graph. Helpers:
childrenOf(graph, parentId)— direct children.resourcesByAccount(graph, accountId).relationshipsOf(graph, resourceId).
Note: containment is both modeled here and rendered — children pack visually inside their parent’s bounds via the pure layout engine in
src/canvas/layout.ts. See Visual Mapping.
Placement scopes
ServiceDefinition.scope (global | region | az | vpc | subnet | project | resource-group) describes where a resource conceptually belongs and is the basis
for placement validation and future layout: IAM is global, an S3 bucket is
region-scoped, a subnet is az-scoped, an EC2 instance must sit inside a subnet,
and so on. The last two are the multi-cloud containers: project is a GCP
project/org-level scope, and resource-group is an Azure resource group (a
mandatory regional container).
Validation & summaries
validateGraph(graph)returns structural errors: duplicate resource ids,parentIds pointing at missing resources, and relationships referencing missingfrom/to. The API enforces this on every write (see Persistence) and it is covered by the model and route tests in CI (see Testing).summarize(graph) → GraphSummaryproduces the lightweight shape list endpoints return (avoids shipping full graphs).emptyGraph(name)produces a minimal valid graph;id/timestamps are assigned on persist.
A separate runtime shape check lives in src/server/graphSchema.ts
(isInfrastructureGraph, hasGraphCollections): the model is a compile-time
contract only, so data crossing a trust boundary (request bodies, JSON on disk)
is structurally validated before being treated as an InfrastructureGraph.