Data model
One SQLite database (bundled — no system dependency), WAL mode, snapshot-scoped
rows with ON DELETE CASCADE from snapshots.
erDiagram
snapshots ||--o{ subscriptions : has
snapshots ||--o{ resource_groups : has
snapshots ||--o{ resources : has
snapshots ||--o{ edges : has
snapshots ||--o{ findings : has
snapshots ||--o{ query_runs : has
snapshots ||--o{ query_results : has
snapshots ||--o{ website_endpoints : has
snapshots ||--o{ website_evidence : has
snapshots ||--o{ website_captures : has
snapshots {
text id PK "uuid"
text created_at "RFC3339"
text tenant_id
text status "running|complete|partial|failed"
text notes
}
resources {
text snapshot_id PK,FK
text id PK "lowercased ARM id"
text display_id "original casing"
text name
text type "lowercased"
text location
text resource_group "lowercased"
text subscription_id
text tags "JSON"
text properties "full JSON bag"
}
edges {
text source_id "lowercased ARM id"
text target_id "lowercased ARM id"
text edge_type "subnet_of, peered_with, ..."
text properties "JSON"
}
findings {
text query_name "provenance"
text category
text severity "high|medium|low|info"
text resource_id
text title
text detail "JSON row"
}
query_runs {
text query_name
int row_count
int duration_ms
text provenance "nullable JSON definition and request scope"
text error
}
query_results {
text query_name
int row_index
text row "raw JSON row"
}
Key points
- Lowercased ids are the join key everywhere (
resources.id, edge endpoints,findings.resource_id);display_idkeeps original casing for display. See architecture for why. query_resultsholds raw rows of the shaped inventory queries (mv-expanded subnets, peerings, …) verbatim — report category tables render straight from them.query_runsis the per-snapshot audit trail: which query ran, how many rows, how long, what failed. Migration 4 adds nullableprovenanceJSON with the executed KQL and its SHA-256, captured query metadata, source details, freshness rule and requested subscriptions/authorization scope. Failed runs retain provenance too. Older rows remain NULL, never backfilled from the current pack. Invalid stored metadata is reported as an error.- Snapshot diff is one
FULL OUTER JOINoverresourcesbetween two snapshot ids (store/snapshots.rs): added / removed / changed (properties text differs).
Website tables were added by migration 3: website_endpoints stores ordered
endpoint associations as JSON, website_evidence stores management responses by
resource and evidence kind, and website_captures stores PNG bytes plus URL,
renderer, dimensions, successful capture fields and the latest attempt outcome.
Their keys include snapshot_id; shared URLs deduplicate within a snapshot.
See Website screenshot pipeline.
The diagram above summarises the core fields rather than listing every column.
Resource identity is the composite (snapshot_id, id), so an ARM resource can
appear in many snapshots. changed compares the stored properties JSON text;
it does not currently report a tag-only, location-only or SKU-only change stored
outside that JSON as changed.
Migrations
Ordered SQL strings in store/schema.rs; meta.schema_version records how
many have run. Append new migrations, never edit existing ones. New tables
must cascade-delete from snapshots.
The desktop wire contract
The Tauri frontend does not see these tables. It receives DTOs from
desktop/src-tauri/src/dto.rs and topology.rs, and their TypeScript
declarations are generated, not written by hand:
cargo test -p azdocs-desktop # rewrites desktop/src/generated.ts
CI runs the same tests and then git diff --exit-code, so a struct change that
is not regenerated fails the build. Never edit generated.ts.
| File | Written by | Holds |
|---|---|---|
desktop/src/generated.ts | ts-rs, via src-tauri/src/bindings.rs | Every DTO the IPC returns |
desktop/src/api-types.ts | Hand | Closed string sets Rust serialises with as_str() — severity, edge kind, snapshot status — which ts-rs cannot infer. Referenced from the structs with #[ts(type = "…")] |
desktop/src/types.ts | Hand | UI-only types, and the re-export surface everything imports |
Two serde behaviours this encodes, both of which had already produced bugs:
#[serde(rename_all)]on an enum renames the variants. The fields of a struct variant needrename_all_fields— without it a payload goes out snake_case while every sibling field is camelCase.Option<T>serialises tonull, not an absent key, so the generated type isfield?: T | null.
Shared database, tenant-scoped access
Tenant profiles do not add tables or rewrite historical snapshots. The existing
snapshots.tenant_id owns each estate; profile references and display names are
editable configuration, not historical keys. Store::with_tenant centralises
filtering for history, latest, ownership checks, previous comparisons and
pruning. Cross-tenant comparisons fail in Rust even for explicit snapshot IDs.
An unscoped mixed-tenant store requires selection for implicit history/latest
operations. tenant_ids enumerates stored tenants so deleted profiles remain
browsable. Profile removal never calls snapshot deletion.
Generated configuration DTOs contain SettingsValues, sparse overrides, tenant
summaries and connection diagnostics. Secret values are input-only command
parameters; legacy plaintext and native credential reads have no response DTO.