Skip to content

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:

  • WebsiteService rows (one per provisioned integration)
  • Domain rows
  • 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.

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";
}

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).

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_tiktok

Stable ids matter because positions are keyed by nodeId in DDB — if the id changed each fetch, dragged positions wouldn’t stick.

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.

const g = await vc.blueprint.get("wb_01J7...");
console.log(g.nodes.length, "nodes,", g.edges.length, "edges");
Terminal window
vantage blueprint get <websiteId>

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:

Terminal window
echo '{"positions":[{"nodeId":"vercel","x":320,"y":180}]}' \
| vantage blueprint positions <websiteId>
  • Services — what becomes a node
  • Domains — what becomes a domain:* node