From 2876420df40c0ad3fbe23e25fd6db2dc8f703212 Mon Sep 17 00:00:00 2001 From: Amp Date: Tue, 15 Sep 2026 05:29:33 +0000 Subject: [PATCH 1/2] BYOC post: lead with what it is, MCP-style hero with VPC outline - Rewrite intro around the security perimeter and outbound-only link - Replace the deployment flow diagram up front with What you get and dashboard deploy steps; move the overview diagram under How it works - New hero renderer in the Introducing lockup style (+ --vpc, --frames) - Hero now served from hero.png to escape the poisoned image.png cache Amp-Thread-ID: https://ampcode.com/threads/T-01a0a1ba-571f-72d8-a85e-924a49ba90b7 Co-authored-by: Nicholas Kissel --- .claude/skills/create-launch-post/SKILL.md | 4 +- .../assets/logos/terraform.svg | 1 + .../scripts/render-byoc-hero.ts | 200 ++++++++++++------ .../page.mdx | 44 ++-- 4 files changed, 155 insertions(+), 94 deletions(-) create mode 100644 .claude/skills/create-launch-post/assets/logos/terraform.svg diff --git a/.claude/skills/create-launch-post/SKILL.md b/.claude/skills/create-launch-post/SKILL.md index 01d731b7..c26eb74f 100644 --- a/.claude/skills/create-launch-post/SKILL.md +++ b/.claude/skills/create-launch-post/SKILL.md @@ -107,8 +107,8 @@ pnpm render-tile-images -- \ ``` 4. When Rivet itself is one of the tiles, pass `src/images/rivet-logos/icon-white.svg` and add `--ink-tile ` (1-based, left to right) for that position. That tile is painted as the product-mark badge — ink fill, `34.375%` radius, white ring and R filling the tile — instead of a black badge floating inside a white app tile. Never place a Rivet or product wordmark in a light tile. The badge is sized level with the smallest neighboring tile. Add `--no-wordmark` to drop the Rivet wordmark above the title when the title or a tile already carries the Rivet mark; the title and tiles shift up to stay balanced. -5. For a diagram hero instead of tiles, start from `scripts/render-byoc-hero.ts` (`pnpm render-byoc-hero -- --output-dir `): a colored, simplified architecture diagram on paper using pine, sage, ink, one accent arrow, and brand-colored provider marks. Copy and adapt it per launch rather than adding flags. -5. Inspect both PNGs. If a title wraps to a third line or a tile crowds the title, shorten the title rather than hand-editing the output. Tile geometry lives in `TILE_LAYOUTS` in the renderer; change it there if a lockup genuinely needs different placement. +5. For the "Introducing" lockup used by the Rivet MCP and Rivet BYOC heroes (eyebrow, ink Rivet badge beside the title, a row of white tiles with brand-colored marks), start from `scripts/render-byoc-hero.ts` (`pnpm render-byoc-hero -- --output-dir [--vpc] [--frames ]`). `--vpc` adds the dashed pine "Your VPC" outline around the tiles; `--frames` writes 1920x1080 animation frames for the GIF/MP4 instead of the stills. Copy and adapt it per launch rather than adding flags. Encode the frames with ffmpeg: MP4 `-framerate 30 -i f%04d.png -c:v libx264 -pix_fmt yuv420p -crf 18 -movflags +faststart`; GIF `-vf "fps=20,scale=1200:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=128:stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" -loop 0`. +6. Inspect both PNGs. If a title wraps to a third line or a tile crowds the title, shorten the title rather than hand-editing the output. Tile geometry lives in `TILE_LAYOUTS` in the renderer; change it there if a lockup genuinely needs different placement. When short code demonstrations would help launch distribution, create a temporary JSON file outside the repository with one to four sections: diff --git a/.claude/skills/create-launch-post/assets/logos/terraform.svg b/.claude/skills/create-launch-post/assets/logos/terraform.svg new file mode 100644 index 00000000..7aafae13 --- /dev/null +++ b/.claude/skills/create-launch-post/assets/logos/terraform.svg @@ -0,0 +1 @@ +Terraform \ No newline at end of file diff --git a/.claude/skills/create-launch-post/scripts/render-byoc-hero.ts b/.claude/skills/create-launch-post/scripts/render-byoc-hero.ts index 29a7e9f7..0470e1f4 100644 --- a/.claude/skills/create-launch-post/scripts/render-byoc-hero.ts +++ b/.claude/skills/create-launch-post/scripts/render-byoc-hero.ts @@ -3,27 +3,30 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { chromium } from "playwright"; -// Rivet BYOC hero: a colored, simplified hero-scale take on the docs' -// ByocArchitectureDiagram. Writes image.png (2048x1024) and social.png -// (2048x1238) plus scene.html. +// Rivet BYOC hero in the "Introducing Rivet MCP" layout: eyebrow, ink Rivet +// badge beside the title, and a row of white tiles carrying brand-colored +// provider marks. Writes image.png (2048x1024), social.png (2048x1238) and +// scene.html. // -// pnpm render-byoc-hero -- --output-dir /tmp/byoc-hero +// pnpm render-byoc-hero -- --output-dir /tmp/byoc-hero [--vpc] +// +// --vpc draws a dashed pine "Your VPC" outline around the tile row. const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const REPO = path.resolve(SCRIPT_DIR, "../../../.."); +const ASSETS = path.resolve(SCRIPT_DIR, "../assets/logos"); const CARD_W = 2048; const CARD_H = 1024; const INK = "#1B1916"; const INK_SOFT = "#56524A"; const PAPER = "#EFEFEF"; -const CREAM = "#F4F1E7"; const PINE = "#2E4034"; -const SAGE = "#93A286"; -const ACCENT = "#CB5A33"; const AWS_ORANGE = "#FF9900"; const GCP_BLUE = "#4285F4"; +const K8S_BLUE = "#326CE5"; +const TERRAFORM_PURPLE = "#7B42BC"; -const strip = (s: string) => s.replace(/<\?xml[^>]*\?>/i, "").replace(/]*>/i, ""); +const strip = (s: string) => s.replace(/<\?xml[^>]*\?>/i, "").replace(/]*>/i, "").replace(/[^<]*<\/title>/i, ""); const ensureViewBox = (svg: string) => { const open = svg.match(/<svg\b[^>]*>/i)?.[0]; if (!open || /\bviewBox=/i.test(open)) return svg; @@ -32,34 +35,89 @@ const ensureViewBox = (svg: string) => { return w && h ? svg.replace(open, open.replace(/<svg\b/i, `<svg viewBox="0 0 ${w} ${h}"`)) : svg; }; const pathData = (svg: string) => svg.match(/\sd="([^"]+)"/)![1]; +const recolor = (svg: string, fill: string) => + /\bfill="/i.test(svg.match(/<svg\b[^>]*>/i)![0]) || /<path\b[^>]*fill=/i.test(svg) + ? svg.replace(/fill="#[0-9a-f]{3,6}"/gi, `fill="${fill}"`) + : svg.replace(/<svg\b/i, `<svg fill="${fill}"`); // The registry AWS mark is one path: four subpaths for "aws" then two for the // smile. Split them so the smile can take the brand orange. function awsMark(svg: string): string { const subs = pathData(svg).split("z").filter((s) => s.trim()).map((s) => s + "z"); const letters = subs.slice(0, 4).join(""); - // Absolute starts of the two smile subpaths, resolved from the relative moves. const smile = "M578.59 368.94" + subs[4].replace(/^m[\d.\- ]+/, "") + "M607.78 335.65" + subs[5].replace(/^m[\d.\- ]+/, ""); return `<svg viewBox="0 0 640 512" xmlns="http://www.w3.org/2000/svg"><path d="${letters}" fill="${INK}"/><path d="${smile}" fill="${AWS_ORANGE}"/></svg>`; } -async function buildHtml(): Promise<string> { - const [font, badgeRaw, awsRaw, gcpRaw] = await Promise.all([ +async function buildHtml(vpc: boolean): Promise<string> { + const [font, badgeRaw, awsRaw, gcpRaw, k8sRaw, tfRaw] = await Promise.all([ readFile(path.join(REPO, "public/fonts/manrope/Manrope-Variable-latin.woff2")), readFile(path.join(REPO, "src/images/rivet-logos/icon-white.svg"), "utf8"), readFile(path.join(REPO, "public/images/registry/deploy-aws-ecs.svg"), "utf8"), readFile(path.join(REPO, "public/images/registry/deploy-gcp-cloud-run.svg"), "utf8"), + readFile(path.join(REPO, "public/images/registry/deploy-kubernetes.svg"), "utf8"), + readFile(path.join(ASSETS, "terraform.svg"), "utf8"), ]); const badge = ensureViewBox(strip(badgeRaw)).replace(/#f0f0f0\b/gi, "#FFFFFF").replace(/#0f0f0f\b/gi, INK); - const aws = awsMark(strip(awsRaw)); - const gcp = strip(gcpRaw).replace(/fill="#1b1916"/i, `fill="${GCP_BLUE}"`); + const tiles: { svg: string; scale: number }[] = [ + { svg: awsMark(strip(awsRaw)), scale: 0.62 }, + { svg: recolor(strip(gcpRaw), GCP_BLUE), scale: 0.56 }, + { svg: recolor(strip(k8sRaw), K8S_BLUE), scale: 0.74 }, + { svg: recolor(strip(tfRaw), TERRAFORM_PURPLE), scale: 0.44 }, + ]; + + const TILE = 140; + const GAP = 28; + const rowW = tiles.length * TILE + (tiles.length - 1) * GAP; + const rowX = Math.round((CARD_W - rowW) / 2); + const rowY = vpc ? 656 : 598; + const tileHtml = tiles + .map((t, i) => { + const inner = Math.round(TILE * t.scale); + return `<div class="tile anim-tile" id="tile${i}" style="left:${rowX + i * (TILE + GAP)}px;top:${rowY}px;width:${TILE}px;height:${TILE}px"> + <div class="mark" style="width:${inner}px;height:${inner}px">${t.svg}</div></div>`; + }) + .join(""); + + const PAD = 44; + const vpcHtml = vpc + ? `<svg class="vpc" id="vpc" viewBox="0 0 ${CARD_W} ${CARD_H}" xmlns="http://www.w3.org/2000/svg" style="transform-origin:${CARD_W / 2}px ${rowY + TILE / 2}px"> + <rect x="${rowX - PAD}" y="${rowY - PAD - 26}" width="${rowW + PAD * 2}" height="${TILE + PAD * 2 + 26}" rx="34" fill="none" stroke="${PINE}" stroke-width="3" stroke-dasharray="14 12"/> + <rect x="${CARD_W / 2 - 92}" y="${rowY - PAD - 44}" width="184" height="36" fill="${PAPER}"/> + <text x="${CARD_W / 2}" y="${rowY - PAD - 16}" text-anchor="middle" font-family="Manrope, sans-serif" font-size="30" font-weight="500" fill="${PINE}">Your VPC</text> + </svg>` + : ""; - // Diagram geometry in card pixels. - const num = (x: number, y: number, n: number, stroke = PINE) => - `<circle cx="${x}" cy="${y}" r="22" fill="${PAPER}" stroke="${stroke}" stroke-width="2.5"/> - <text x="${x}" y="${y + 9}" text-anchor="middle" font-size="26" font-weight="600" fill="${stroke}">${n}</text>`; + // Animation: every frame is a pure function of t (seconds), driven from + // Playwright in --frames mode. Stills never call render(). + const script = `<script> + const clamp = (x) => Math.min(1, Math.max(0, x)); + const outCubic = (x) => 1 - Math.pow(1 - x, 3); + const outBack = (x) => { const c1 = 1.4, c3 = c1 + 1; return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2); }; + const seg = (t, a, d) => clamp((t - a) / d); + window.render = (t) => { + const fade = 1 - outCubic(seg(t, 4.9, 0.55)); + const h = outCubic(seg(t, 0.0, 0.6)); + for (const id of ["eyebrow", "lockup"]) { + const el = document.getElementById(id); + el.style.opacity = String(h * fade); + el.style.transform = "translateY(" + (1 - h) * 26 + "px)"; + } + Array.from(document.querySelectorAll(".anim-tile")).forEach((el, i) => { + const p = seg(t, 0.55 + i * 0.16, 0.5); + el.style.opacity = String(outCubic(p) * fade); + el.style.transform = "scale(" + (0.6 + 0.4 * outBack(p)) + ")"; + }); + const vpc = document.getElementById("vpc"); + if (vpc) { + const p = outCubic(seg(t, 1.35, 0.6)); + vpc.style.opacity = String(p * fade); + vpc.style.transform = "scale(" + (0.97 + 0.03 * p) + ")"; + } + }; + </script>`; return `<!doctype html><html><head><meta charset="utf-8"><style> @font-face { font-family: "Manrope"; src: url("data:font/woff2;base64,${font.toString("base64")}") format("woff2"); font-weight: 200 800; } @@ -67,64 +125,72 @@ async function buildHtml(): Promise<string> { html, body { margin: 0; background: ${PAPER}; } .stage { position: relative; width: ${CARD_W}px; height: ${CARD_H}px; overflow: hidden; background: ${PAPER}; } .card { position: absolute; left: 0; top: 0; width: ${CARD_W}px; height: ${CARD_H}px; font-family: "Manrope", sans-serif; color: ${INK}; } - /* No title on the hero; the post title carries it. The diagram is drawn - in the coordinates it had under the title and recentred here. */ - .scene { position: absolute; inset: 0; transform: translateY(-68px); } - svg.diagram { position: absolute; left: 0; top: 0; width: ${CARD_W}px; height: ${CARD_H}px; font-family: "Manrope", sans-serif; } - .logo { position: absolute; display: flex; align-items: center; justify-content: center; } - .logo svg { display: block; height: 100%; width: auto; } - </style></head><body><div class="stage" id="stage"><div class="card" id="card"><div class="scene"> - <svg class="diagram" viewBox="0 0 ${CARD_W} ${CARD_H}" xmlns="http://www.w3.org/2000/svg"> - <defs> - <marker id="ah-pine" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0 0L10 5L0 10z" fill="${PINE}"/></marker> - <marker id="ah-accent" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0 0L10 5L0 10z" fill="${ACCENT}"/></marker> - </defs> - <g fill="${INK}"> - <!-- Your VPC --> - <rect x="944" y="300" width="900" height="560" rx="30" fill="${SAGE}" fill-opacity="0.18" stroke="${PINE}" stroke-width="3" stroke-dasharray="16 12"/> - <text x="992" y="366" font-size="32" font-weight="500" fill="${PINE}">Your VPC</text> - - <!-- Rivet Cloud --> - <rect x="204" y="490" width="380" height="180" rx="22" fill="${INK}"/> - <text x="394" y="642" text-anchor="middle" font-size="38" font-weight="600" fill="#FFFFFF">Rivet Cloud</text> - - <!-- Outbound only --> - <path d="M944 580 H590" fill="none" stroke="${ACCENT}" stroke-width="4" marker-end="url(#ah-accent)"/> - <text x="767" y="548" text-anchor="middle" font-size="26" font-weight="500" fill="${ACCENT}">Outbound only</text> - <text x="767" y="622" text-anchor="middle" font-size="24" fill="${INK_SOFT}">Updates and status</text> - - <!-- Deployment inside the VPC --> - <rect x="1024" y="420" width="740" height="380" rx="22" fill="#FFFFFF" fill-opacity="0.7" stroke="${PINE}" stroke-width="2"/> - <rect x="1064" y="460" width="660" height="88" rx="16" fill="#FFFFFF" stroke="${INK}" stroke-width="2.5"/> - <text x="1394" y="516" text-anchor="middle" font-size="34" font-weight="600">Rivet operator</text> - <rect x="1064" y="572" width="660" height="88" rx="16" fill="${PINE}"/> - <text x="1394" y="628" text-anchor="middle" font-size="34" font-weight="600" fill="${CREAM}">Rivet control plane</text> - <rect x="1064" y="684" width="660" height="88" rx="16" fill="#FFFFFF" stroke="${INK}" stroke-width="2.5"/> - <text x="1394" y="740" text-anchor="middle" font-size="34" font-weight="600">FoundationDB</text> - <text x="1394" y="836" text-anchor="middle" font-size="24" fill="${INK_SOFT}">Managed by Rivet</text> - </g> - </svg> - <!-- Rivet badge on the Rivet Cloud card --> - <div class="logo" style="left:364px;top:522px;width:60px;height:60px">${badge}</div> - <!-- Provider marks, top-right of the VPC --> - <div class="logo" style="left:1636px;top:330px;height:48px">${aws}</div> - <div class="logo" style="left:1748px;top:330px;height:48px">${gcp}</div> - </div></div></div></body></html>`; + .eyebrow { position: absolute; top: 238px; left: 0; right: 0; margin: 0; text-align: center; font-size: 44px; line-height: 1; font-weight: 500; color: ${INK_SOFT}; } + .lockup { position: absolute; top: 322px; left: 0; right: 0; display: flex; align-items: center; justify-content: center; gap: 56px; } + .badge { width: 186px; height: 186px; border-radius: 34.375%; background: ${INK}; display: flex; align-items: center; justify-content: center; } + .badge svg { display: block; width: 100%; height: 100%; } + h1 { margin: 0; font-size: 156px; line-height: 1; letter-spacing: -0.015em; font-weight: 500; } + .tile { position: absolute; border-radius: 30%; background: #FFFFFF; border: 2px solid rgba(27, 25, 22, 0.1); display: flex; align-items: center; justify-content: center; } + .mark { display: flex; align-items: center; justify-content: center; } + .mark svg { display: block; width: 100%; height: 100%; } + svg.vpc { position: absolute; left: 0; top: 0; width: ${CARD_W}px; height: ${CARD_H}px; } + .card { transform-origin: 0 0; } + </style></head><body><div class="stage" id="stage"><div class="card" id="card"> + <p class="eyebrow" id="eyebrow">Introducing</p> + <div class="lockup" id="lockup"><div class="badge">${badge}</div><h1>Rivet BYOC</h1></div> + ${vpcHtml} + ${tileHtml} + </div></div>${script}</body></html>`; } -function parseOutputDir(argv: string[]): string { +function parseArgs(argv: string[]): { outputDir: string; vpc: boolean; frames?: string } { const args = argv[0] === "--" ? argv.slice(1) : argv; - const index = args.indexOf("--output-dir"); - const value = index >= 0 ? args[index + 1] : undefined; - if (!value) throw new Error("Usage: pnpm render-byoc-hero -- --output-dir <path>"); - return path.resolve(value); + const opt = (flag: string) => { + const index = args.indexOf(flag); + return index >= 0 ? args[index + 1] : undefined; + }; + const value = opt("--output-dir"); + if (!value) throw new Error("Usage: pnpm render-byoc-hero -- --output-dir <path> [--vpc] [--frames <dir>]"); + const frames = opt("--frames"); + return { outputDir: path.resolve(value), vpc: args.includes("--vpc"), frames: frames ? path.resolve(frames) : undefined }; +} + +// 1920x1080 frames for the GIF/MP4: the 2048x1024 card scaled to 1920 wide and +// centered vertically. Encode with ffmpeg afterwards (see SKILL.md). +async function renderFrames(html: string, dir: string) { + const W = 1920; + const H = 1080; + const FPS = 30; + const DURATION = 5.6; + await mkdir(dir, { recursive: true }); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: W, height: H }, deviceScaleFactor: 1 }); + await page.setContent(html, { waitUntil: "load" }); + await page.evaluate(() => document.fonts.ready); + await page.evaluate(({ w, h }) => { + const scale = w / 2048; + const stage = document.getElementById("stage")!; + stage.style.width = w + "px"; + stage.style.height = h + "px"; + const card = document.getElementById("card")!; + card.style.top = Math.round((h - 1024 * scale) / 2) + "px"; + card.style.transform = "scale(" + scale + ")"; + }, { w: W, h: H }); + const frames = Math.round(DURATION * FPS); + for (let i = 0; i < frames; i++) { + await page.evaluate((t) => (window as unknown as { render: (t: number) => void }).render(t), i / FPS); + await page.screenshot({ path: path.join(dir, `f${String(i).padStart(4, "0")}.png`) }); + } + await browser.close(); + console.log(`wrote ${frames} frames to ${dir}`); } async function main() { - const OUT = parseOutputDir(process.argv.slice(2)); + const { outputDir: OUT, vpc, frames } = parseArgs(process.argv.slice(2)); await mkdir(OUT, { recursive: true }); - const html = await buildHtml(); + const html = await buildHtml(vpc); await writeFile(path.join(OUT, "scene.html"), html); + if (frames) return renderFrames(html, frames); const browser = await chromium.launch(); for (const target of [ { name: "image", w: 2048, h: 1024 }, diff --git a/src/content/posts/2026-09-15-introducing-rivet-byoc/page.mdx b/src/content/posts/2026-09-15-introducing-rivet-byoc/page.mdx index d95edc40..95e62c50 100644 --- a/src/content/posts/2026-09-15-introducing-rivet-byoc/page.mdx +++ b/src/content/posts/2026-09-15-introducing-rivet-byoc/page.mdx @@ -2,35 +2,38 @@ author: nicholas-kissel published: "2026-09-15" category: changelog -image: { file: "image.png?v=7" } +image: { file: "hero.png" } keywords: ["byoc", "bring your own cloud", "vpc", "aws", "google cloud", "terraform", "kubernetes", "enterprise", "self-host", "rivet actors"] title: "Introducing Rivet BYOC" description: "Run the Rivet control plane inside your own AWS or Google Cloud VPC, fully managed by Rivet. Your data stays in your account and Rivet Cloud never needs inbound access." --- -import ByocArchitectureDiagram from '@/components/docs/ByocArchitectureDiagram.astro'; +import ByocOverviewDiagram from '@/components/docs/ByocOverviewDiagram.astro'; import ByocRegionsDiagram from '@/components/docs/ByocRegionsDiagram.astro'; -Today we're releasing **Rivet BYOC** (Bring Your Own Cloud): the Rivet control plane deployed inside your own AWS or Google Cloud VPC and managed by Rivet. +Today we're releasing **Rivet BYOC** (Bring Your Own Cloud): the Rivet control plane deployed inside your own AWS or Google Cloud VPC, managed by Rivet. Your data and compute never leave your security perimeter, and the only connection to Rivet Cloud is outbound. -- **Rivet Cloud**: Rivet runs everything. -- **Self-host**: you run everything. -- **BYOC**: your data and compute stay in your account. Rivet handles deployment, updates, and maintenance. +## What you get -## How deployments work +- **Your account, your data**: the control plane, FoundationDB, and your actors run in your VPC +- **Outbound only**: no inbound rules from Rivet Cloud. Your admin token stays in your cloud secret manager. +- **Managed by Rivet**: deployment, updates, and maintenance are handled for you +- **Public or private**: expose Rivet over HTTPS on your own hostname, or keep it on your private network +- **Dashboard included**: manage your actors from the Rivet dashboard, tunneled to your deployment -<ByocArchitectureDiagram /> +## Deploy from the dashboard -A Rivet operator runs in your Kubernetes cluster and connects outbound to Rivet Cloud. +1. Open the [Rivet dashboard](https://dashboard.rivet.dev), click **Create Project**, and select **BYOC** +2. Download the setup kit and cluster config from your project's BYOC page +3. Set your account, regions, and hostname in `terraform.tfvars.json`, then run `terraform apply` -1. Request a deployment or update -2. The operator pulls instructions from Rivet Cloud -3. The operator applies the update inside your cluster -4. The operator reports status back to Rivet Cloud +Full steps are in the [quickstart](/cloud/byoc/quickstart). -- **No inbound access**: your VPC needs no inbound rules from Rivet Cloud. Your admin token stays in your cloud secret manager. -- **Public or private**: expose Rivet over HTTPS on your own hostname, or keep it on your private network. -- **Dashboard included**: manage your actors from the Rivet dashboard, tunneled to your deployment. +## How it works + +<ByocOverviewDiagram /> + +A Rivet operator runs in your Kubernetes cluster. It pulls commands from Rivet Cloud, applies updates to the control plane inside your cluster, and reports status back. Rivet Cloud never connects in. ## Multi-region @@ -41,15 +44,6 @@ A Rivet operator runs in your Kubernetes cluster and connects outbound to Rivet - Regions talk over private connectivity and share a container registry - Workers connect to their regional endpoint -## BYOC for your product too - -If you build on Rivet, BYOC is also how you offer BYOC to your own customers. - -- Create a BYOC project per customer. They apply the setup kit in their AWS or Google Cloud account. -- Rivet brings up the control plane in their VPC and keeps it updated. -- Deploy your workers against it the same way you deploy against Rivet Cloud. Same build everywhere. -- Every deployment is isolated: its own VPC, cluster, storage, and admin token. - ## Availability - Available today on AWS and Google Cloud with [Rivet Enterprise Edition](/enterprise/) From 1f230dc1e40a6f15c01f9be172262deada812395 Mon Sep 17 00:00:00 2001 From: Amp <amp@ampcode.com> Date: Tue, 15 Sep 2026 05:33:23 +0000 Subject: [PATCH 2/2] BYOC hero animation: draw the VPC outline around the tiles Amp-Thread-ID: https://ampcode.com/threads/T-01a0a1ba-571f-72d8-a85e-924a49ba90b7 Co-authored-by: Nicholas Kissel <nicholas@rivet.gg> --- .../scripts/render-byoc-hero.ts | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/.claude/skills/create-launch-post/scripts/render-byoc-hero.ts b/.claude/skills/create-launch-post/scripts/render-byoc-hero.ts index 0470e1f4..06870fa5 100644 --- a/.claude/skills/create-launch-post/scripts/render-byoc-hero.ts +++ b/.claude/skills/create-launch-post/scripts/render-byoc-hero.ts @@ -83,11 +83,20 @@ async function buildHtml(vpc: boolean): Promise<string> { const PAD = 44; const vpcHtml = vpc - ? `<svg class="vpc" id="vpc" viewBox="0 0 ${CARD_W} ${CARD_H}" xmlns="http://www.w3.org/2000/svg" style="transform-origin:${CARD_W / 2}px ${rowY + TILE / 2}px"> - <rect x="${rowX - PAD}" y="${rowY - PAD - 26}" width="${rowW + PAD * 2}" height="${TILE + PAD * 2 + 26}" rx="34" fill="none" stroke="${PINE}" stroke-width="3" stroke-dasharray="14 12"/> - <rect x="${CARD_W / 2 - 92}" y="${rowY - PAD - 44}" width="184" height="36" fill="${PAPER}"/> - <text x="${CARD_W / 2}" y="${rowY - PAD - 16}" text-anchor="middle" font-family="Manrope, sans-serif" font-size="30" font-weight="500" fill="${PINE}">Your VPC</text> - </svg>` + ? (() => { + // The dashed outline is masked by a solid stroke on the same geometry; + // the animation shortens that stroke's dash so the line draws on + // clockwise from the top-left corner. + const geom = `x="${rowX - PAD}" y="${rowY - PAD - 26}" width="${rowW + PAD * 2}" height="${TILE + PAD * 2 + 26}" rx="34" fill="none"`; + return `<svg class="vpc" id="vpc" viewBox="0 0 ${CARD_W} ${CARD_H}" xmlns="http://www.w3.org/2000/svg"> + <defs><mask id="vpc-mask"><rect id="vpc-draw" ${geom} stroke="#FFFFFF" stroke-width="12"/></mask></defs> + <rect ${geom} stroke="${PINE}" stroke-width="3" stroke-dasharray="14 12" mask="url(#vpc-mask)"/> + <g id="vpc-label"> + <rect x="${CARD_W / 2 - 92}" y="${rowY - PAD - 44}" width="184" height="36" fill="${PAPER}"/> + <text x="${CARD_W / 2}" y="${rowY - PAD - 16}" text-anchor="middle" font-family="Manrope, sans-serif" font-size="30" font-weight="500" fill="${PINE}">Your VPC</text> + </g> + </svg>`; + })() : ""; // Animation: every frame is a pure function of t (seconds), driven from @@ -112,9 +121,13 @@ async function buildHtml(vpc: boolean): Promise<string> { }); const vpc = document.getElementById("vpc"); if (vpc) { - const p = outCubic(seg(t, 1.35, 0.6)); - vpc.style.opacity = String(p * fade); - vpc.style.transform = "scale(" + (0.97 + 0.03 * p) + ")"; + vpc.style.opacity = String(fade); + const draw = document.getElementById("vpc-draw"); + const len = draw.getTotalLength(); + const p = outCubic(seg(t, 1.3, 1.1)); + draw.setAttribute("stroke-dasharray", String(len)); + draw.setAttribute("stroke-dashoffset", String(len * (1 - p))); + document.getElementById("vpc-label").style.opacity = String(outCubic(seg(t, 2.2, 0.4))); } }; </script>`;