Blueprint
import { Aside } from ‘@astrojs/starlight/components’;
The Blueprint is the pan/zoom canvas you see at
/admin/websites/:id in the portal. It shows every service attached to
a Website as a node, with edges connecting related services
(GitHub → Vercel via build, Stripe → Vercel via billing, etc.).
The Blueprint is derived — there’s no separate “blueprint nodes” table. Every fetch joins:
WebsiteServicerows (one per provisioned integration)Domainrows- Website billing status + Stripe subscription
- The owning Client (when assigned)
…into a BlueprintGraph payload. Only positions persist (in
vantage-website-blueprint-layout), so dragging nodes around survives
page reload but adding a service doesn’t require a separate “register
this on the blueprint” step.
Data shape
Section titled “Data shape”interface BlueprintGraph { websiteId: string; nodes: BlueprintNode[]; edges: BlueprintEdge[];}
interface BlueprintNode { nodeId: string; type: BlueprintServiceType; // "github" | "vercel" | "commerce" | ... status: "ok" | "warn" | "err" | "pending"; meta?: string; // short summary (domain name, etc.) fields: Array<{ key: string; value: string }>; x: number; y: number; // canvas position}
interface BlueprintEdge { edgeId: string; from: { nodeId: string; portId: string }; to: { nodeId: string; portId: string }; kind: "build" | "data" | "traffic" | "billing" | "auth";}Why derived rather than stored?
Section titled “Why derived rather than stored?”Storing the graph would duplicate state. If you added a Shopify integration through OAuth but the blueprint graph row wasn’t updated, the canvas would lie. By deriving the graph from the underlying tables on every fetch, the canvas can’t drift — what you see is exactly what’s provisioned.
The downside: re-derivation has a per-fetch cost. We accept it because fetches are interactive (one user opening one canvas) and the joins are cheap (~6 DDB queries per page).
Node ids are stable across fetches
Section titled “Node ids are stable across fetches”Always-present services use the bare type: github, vercel, cms, cognito, stripe
Optional / multi-instance services use type:discriminator: domain:joesplumbing.com resend:mail.joesplumbing.com channel_tiktokStable ids matter because positions are keyed by nodeId in DDB — if
the id changed each fetch, dragged positions wouldn’t stick.
Edge kinds
Section titled “Edge kinds”Edges are color-coded by kind in the portal:
build— code → deploy (GitHub → Vercel)data— data flow (Commerce → Vercel template via Storefront API)traffic— request routing (Domain → Vercel)billing— money flow (Stripe → Website)auth— identity flow (Cognito → Vercel for client portal)
The portal computes edges at render time from the canonical relationships between service types — there’s no “draw a line between A and B” UI; structure follows what’s actually wired.
Fetching the graph programmatically
Section titled “Fetching the graph programmatically”const g = await vc.blueprint.get("wb_01J7...");console.log(g.nodes.length, "nodes,", g.edges.length, "edges");vantage blueprint get <websiteId>Persisting positions
Section titled “Persisting positions”The portal POSTs positions after each drag-stop. From code:
await vc.blueprint.setPositions("wb_01J7...", { positions: [ { nodeId: "vercel", x: 320, y: 180 }, { nodeId: "github", x: 120, y: 180 }, ],});CLI variant accepts JSON on stdin:
echo '{"positions":[{"nodeId":"vercel","x":320,"y":180}]}' \ | vantage blueprint positions <websiteId>