---
title: 'Harbor: Stripe’s AI-assisted prototyping tool'
summary: 'Harbor is the internal tool Stripe teams use to prototype with an AI agent. This post covers how it compiles prototypes in the browser, how the agent inspects its own work, and how comments stay anchored when the page is rewritten.'
authors:
  - 'Cristian Rivera'
  - 'Sai Samant'
date: '2026-09-14'
topics:
  - 'AI'
  - 'Developer Productivity'
  - 'Engineering'
  - 'AI Research'
type: 'Blog'
---

At Stripe, we’ve integrated AI into many of our workflows. We created [Minions](https://stripe.dev/blog/minions-stripes-one-shot-end-to-end-coding-agents) that write code and built our [Knowledge AI Platform](https://stripe.dev/blog/meet-stripes-knowledge-ai-platform) to help Stripes with a wide range of knowledge work. Many tasks are now faster and easier to complete thanks to these AI assistants.

An area where we haven’t seen as many improvements from AI has been prototyping. When making changes to [stripe.com](https://stripe.com), our team often wants more information to evaluate the feasibility of a new design idea, but can’t always invest in developing a full engineering prototype.

One engineer built the first version of Harbor in about 40 days to solve that problem. We wanted to see whether AI could help designers turn a static design into something dynamic, without needing to understand code.

The result is an interactive, browser-based platform where designers can use AI to rapidly develop shareable prototypes using Stripe’s design systems and realistic mock data.

Harbor immediately improved the design phase of [stripe.com](https://stripe.com) projects, increasing engagement and driving clarity around the final vision. In one of the first projects to use Harbor, a designer built a new product page experience, including its animated hero. Engineers, product managers, and leaders interacted with it in real time, leading to quick iteration and consensus. When the page shipped, the animation carried over nearly one-for-one from the prototype.

<div style="display:flex;flex-wrap:wrap;gap:16px;margin:24px 0 0"><figure style="flex:1 1 260px;margin:0"><figcaption style="font-family:sohne-mono,'Courier New',Courier,monospace;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--bodyTextColor,#1e1e1e);margin:0 0 8px">Harbor prototype</figcaption><img src="/images/harbor-stripes-ai-assisted-prototyping-tool/prototype-hero.png" alt="The hero as prototyped in Harbor: a phone showing an add-funds keypad, over a dashboard, on a violet gradient." style="width:100%;height:auto;display:block"/></figure><figure style="flex:1 1 260px;margin:0"><figcaption style="font-family:sohne-mono,'Courier New',Courier,monospace;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--bodyTextColor,#1e1e1e);margin:0 0 8px">Shipped stripe.com page</figcaption><img src="/images/harbor-stripes-ai-assisted-prototyping-tool/shipped-hero.png" alt="The same hero on the shipped page: a phone over a dashboard, on the same gradient, with the animation carried across." style="width:100%;height:auto;display:block"/></figure></div>

Stripes across the company adapted Harbor for their own needs. Finance teams started presenting monthly actuals and business reviews as clickable prototypes instead of slide decks, a risk operations analyst turned a handle time analysis into a dashboard the rest of the team could inspect, and account executives built pricing proposals, battlecards, and ROI calculators for specific prospects.

![The Harbor builder: a monthly actuals review prototype rendering on the left, and the chat panel on the right showing the agent reading the KPI row, the revenue chart, and the variance table.](/images/harbor-stripes-ai-assisted-prototyping-tool/builder-monthly-actuals.png)

> A monthly actuals review built in Harbor: the running prototype on the left, the agent and its tool calls on the right.

Since May 2026, 3,000+ Stripes have created 12,000 prototypes, and 25% of Stripes have created at least one prototype in Harbor. This adoption came with more requests for increased collaboration within a Harbor prototype and interoperability with other AI tooling. Read on to learn how we built this functionality.

## Building Harbor for collaboration with people and agents

Harbor’s interface is a browser tab with the running prototype and a chat panel. The author selects a design system for the prototype, setting its packages and starting files, then describes what they want. From there, Harbor works like this:

- The agent writes a multi-file source tree, and the page compiles and renders in the browser rather than on a server.
- The agent can read the rendered page, list and inspect elements, click, type, scroll, switch the viewport between mobile, tablet, and desktop widths, and take screenshots.
- When the prototype is ready, the author publishes it to a shareable URL. There’s one published version at a time, and every view compiles that copy fresh in the viewer’s own browser.

<svg xmlns="http://www.w3.org/2000/svg" id="fig-lifecycle" viewBox="0 0 708 428" style="display:block;width:100%;height:auto;margin:24px 0 0" role="img" aria-labelledby="lc-t lc-d">
<title id="lc-t">Harbor’s render lifecycle: From a prompt to a page running in the browser</title>
<desc id="lc-d">The agent writes a multi-file source tree from the prompt. Drydock compiles it in the browser and returns a running page with a report. The agent reads the page, switches viewport, screenshots, and edits files again. On publish, one snapshot is shared and every viewer’s browser compiles that copy.</desc>
<defs><pattern id="lc-grid" width="20" height="20" patternUnits="userSpaceOnUse"><circle cx="10" cy="10" r="1" style="fill:var(--artGridDotColor,var(--windowBarText,#1e1e1e))" opacity=".2"/></pattern></defs>
<style>
#fig-lifecycle-cap{display:none;margin:8px 0 0}
#fig-lifecycle text{font-family:'sohne-var','Helvetica Neue',Arial,sans-serif;fill:var(--bodyTextColor,#1e1e1e)}
#fig-lifecycle .m{font-family:'sohne-mono',ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:300;letter-spacing:-.012em;font-size:12px;text-transform:uppercase;fill:var(--squareTextColor,#1e1e1e)}
#fig-lifecycle .s10{font-size:10px}#fig-lifecycle .fig{font-size:8px;fill:var(--windowBarText,#1e1e1e)}#fig-lifecycle .bt{fill:var(--windowBarText,#1e1e1e)}
#fig-lifecycle .mute{fill-opacity:.55}#fig-lifecycle .tt{fill:var(--tagTextColor,#1e1e1e)}#fig-lifecycle .lt{fill:var(--invertedTextColor,#1e1e1e)}#fig-lifecycle .light{fill:var(--bodyTextColor,#1e1e1e)}
#fig-lifecycle .h{fill:none;stroke:var(--artStroke,#1e1e1e);stroke-width:1;shape-rendering:crispEdges}
#fig-lifecycle .win{fill:var(--artBackground,transparent);stroke:var(--artStroke,#1e1e1e);stroke-width:1}#fig-lifecycle .frame{fill:var(--windowFrameBG,#dcdcdc)}#fig-lifecycle .ico{fill:var(--windowBarText,#1e1e1e)}
#fig-lifecycle .canvas{stroke:var(--artStroke,#1e1e1e);stroke-width:1}#fig-lifecycle .pane{fill:var(--artForeground,#e8e8e8)}
#fig-lifecycle .tag{fill:var(--artForeground,#e8e8e8);stroke:var(--tagBorderColor,#1e1e1e44);stroke-width:1;stroke-dasharray:1 1;rx:3}#fig-lifecycle .lime{fill:var(--tagHighlightColor,#c4e817)}
#fig-lifecycle .term{fill:var(--codeBG,#f5f5f5);stroke:var(--codeBorderColor,#d5d5d5);stroke-width:1;rx:4}#fig-lifecycle .termf{fill:var(--codeBG,#f5f5f5)}#fig-lifecycle .hc{fill:none;stroke:var(--codeBorderColor,#d5d5d5);stroke-width:1;shape-rendering:crispEdges;vector-effect:non-scaling-stroke}
#fig-lifecycle .pinbg{fill:var(--artForeground,#e8e8e8)}#fig-lifecycle .sq{fill:var(--artStroke,#1e1e1e)}#fig-lifecycle .sql{fill:var(--bodyTextColor,#1e1e1e)}#fig-lifecycle .sqo{fill:none;stroke:var(--artStroke,#1e1e1e);stroke-width:1}#fig-lifecycle .sqol{fill:none;stroke:var(--bodyTextColor,#1e1e1e);stroke-width:1}#fig-lifecycle .sqd{fill:none;stroke:var(--artStroke,#1e1e1e);stroke-width:1;stroke-dasharray:1.5 1.5;stroke-opacity:.6}
#fig-lifecycle .card{fill:var(--artForeground,#e8e8e8);stroke:var(--artStroke,#1e1e1e);stroke-width:1;shape-rendering:crispEdges}
#fig-lifecycle .dash{fill:none;stroke:var(--artStroke,#1e1e1e);stroke-width:1;stroke-dasharray:3 3}
#fig-lifecycle .h,#fig-lifecycle .win,#fig-lifecycle .canvas,#fig-lifecycle .tag,#fig-lifecycle .term,#fig-lifecycle .card,#fig-lifecycle .sqo,#fig-lifecycle .sqol,#fig-lifecycle .sqd,#fig-lifecycle .dash{vector-effect:non-scaling-stroke}
#fig-lifecycle *{animation-duration:30s;animation-iteration-count:infinite;animation-timing-function:cubic-bezier(.2,.8,.2,1);animation-fill-mode:both}
#fig-lifecycle .scene{animation-name:lc-scene}#fig-lifecycle .f2{animation-name:lc-fi2}#fig-lifecycle .f3{animation-name:lc-fi3}#fig-lifecycle .f4{animation-name:lc-fi4}#fig-lifecycle .a3{animation-name:lc-a3}#fig-lifecycle .arrow1{animation-name:lc-a1}#fig-lifecycle .cl1{animation-name:lc-cl1}#fig-lifecycle .cl2{animation-name:lc-cl2}#fig-lifecycle .cl3{animation-name:lc-cl3}#fig-lifecycle .cl4{animation-name:lc-cl4}#fig-lifecycle .blink{animation-name:lc-blink;animation-duration:1s;animation-timing-function:steps(1)}#fig-lifecycle .arrow2{animation-name:lc-a2}#fig-lifecycle .p1{animation-name:lc-pgel1}#fig-lifecycle .p2{animation-name:lc-pgel2}#fig-lifecycle .p3{animation-name:lc-pgel3}#fig-lifecycle .p4{animation-name:lc-pgel4}#fig-lifecycle .rep{animation-name:lc-rep}#fig-lifecycle .insp{animation-name:lc-insp}#fig-lifecycle .loopa{animation-name:lc-loopa}#fig-lifecycle .pub{animation-name:lc-pub}#fig-lifecycle .s1{animation-name:lc-s1}#fig-lifecycle .s2{animation-name:lc-s2}#fig-lifecycle .s3{animation-name:lc-s3}#fig-lifecycle .s4{animation-name:lc-s4}#fig-lifecycle .s5{animation-name:lc-s5}
@keyframes lc-scene{0%{opacity:0}4.44%,94.44%{opacity:1}98.89%,100%{opacity:0}}@keyframes lc-fi2{0%,9.45%{opacity:0;transform:translateX(-6px)}12.22%,100%{opacity:1;transform:none}}@keyframes lc-fi3{0%,12.23%{opacity:0;transform:translateX(-6px)}15.00%,100%{opacity:1;transform:none}}@keyframes lc-fi4{0%,15.00%{opacity:0;transform:translateX(-6px)}17.77%,100%{opacity:1;transform:none}}@keyframes lc-a3{0%,17%{opacity:0}19.44%,100%{opacity:1}}@keyframes lc-a1{0%,22.22%{opacity:0}25%,100%{opacity:1}}@keyframes lc-cl1{0%,30.00%{opacity:0}32.22%,100%{opacity:1}}@keyframes lc-cl2{0%,32.78%{opacity:0}35.00%,100%{opacity:1}}@keyframes lc-cl3{0%,35.56%{opacity:0}37.78%,100%{opacity:1}}@keyframes lc-cl4{0%,38.33%{opacity:0}40.55%,100%{opacity:1}}@keyframes lc-blink{0%,49%{opacity:1}50%,100%{opacity:0}}@keyframes lc-a2{0%,41.11%{opacity:0}43.89%,100%{opacity:1}}@keyframes lc-pgel1{0%,48.33%{opacity:0;transform:translateY(4px)}51.67%,100%{opacity:1;transform:none}}@keyframes lc-pgel2{0%,50.00%{opacity:0;transform:translateY(4px)}53.34%,100%{opacity:1;transform:none}}@keyframes lc-pgel3{0%,51.66%{opacity:0;transform:translateY(4px)}55.00%,100%{opacity:1;transform:none}}@keyframes lc-pgel4{0%,53.33%{opacity:0;transform:translateY(4px)}56.67%,100%{opacity:1;transform:none}}@keyframes lc-rep{0%,55%{opacity:0}57.78%,100%{opacity:1}}@keyframes lc-insp{0%,66.67%{opacity:0}69.44%,100%{opacity:1}}@keyframes lc-loopa{0%,78.89%{opacity:0}81.67%,85.56%{opacity:1}87.78%,100%{opacity:0}}@keyframes lc-pub{0%,85.56%{opacity:0}88.33%,100%{opacity:1}}@keyframes lc-s1{0%,4.44%{opacity:0}6.67%,25%{opacity:1}27.22%,100%{opacity:0}}@keyframes lc-s2{0%,27.22%{opacity:0}29.44%,45%{opacity:1}47.22%,100%{opacity:0}}@keyframes lc-s3{0%,47.22%{opacity:0}49.44%,65.56%{opacity:1}67.78%,100%{opacity:0}}@keyframes lc-s4{0%,67.78%{opacity:0}70%,84.44%{opacity:1}86.67%,100%{opacity:0}}@keyframes lc-s5{0%,86.67%{opacity:0}88.89%,100%{opacity:1}}
@media (prefers-reduced-motion:reduce){#fig-lifecycle *{animation:none !important}#fig-lifecycle .rmh{display:none}}
@media (max-width:600px){#fig-lifecycle .caps,#fig-lifecycle .mob-hide{display:none}#fig-lifecycle-cap{display:block;font-family:'sohne-mono',ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;font-weight:300;letter-spacing:-.012em;text-transform:uppercase;line-height:1.4;color:var(--squareTextColor,#1e1e1e)}}
</style>
<text x="0" y="16" class="m">/ render lifecycle</text>
<path d="M0 24L708 24" class="h"/>
<g class="scene">
<rect x="0.5" y="36.5" width="707" height="359" class="win"/>
<rect x="1" y="37" width="706" height="20" class="frame"/>
<path transform="translate(5,41)" d="M11 0H1V1H0V11H1V12H11V11H12V1H11V0ZM11 1V11H1V1H11ZM6 7H10V8H6V7ZM3 7H2V8H3V7ZM4 6V7H3V6H4ZM4 5V6H5V5H4ZM3 4H4V5H3V4ZM3 4V3H2V4H3Z" class="ico" fill-rule="evenodd"/>
<path transform="translate(691,41)" d="M-8.74228e-08 10L8 10L8 9L11 9L11 10L12 10L12 11L11 11L11 12L8 12L8 11L-4.37113e-08 11L-8.74228e-08 10ZM-3.93402e-07 3L-4.37114e-07 2L8 2L8 1L11 1L11 2L12 2L12 3L11 3L11 4L8 4L8 3L-3.93402e-07 3ZM1 6L-2.62268e-07 6L-2.18557e-07 7L1 7L1 8L4 8L4 7L12 7L12 6L4 6L4 5L1 5L1 6ZM9 2L10 2L10 3L9 3L9 2ZM10 10L9 10L9 11L10 11L10 10ZM2 6L3 6L3 7L2 7L2 6Z" class="ico" fill-rule="evenodd"/>
<text x="354" y="50.1" class="m fig" text-anchor="middle">[ fig. 1 ]</text>
<rect x="1" y="57" width="4" height="338" class="frame"/>
<rect x="703" y="57" width="4" height="338" class="frame"/>
<rect x="1" y="391" width="706" height="4" class="frame"/>
<rect x="5.5" y="57.5" width="697" height="333" rx="2" fill="url(#lc-grid)" class="canvas"/>
<text x="17" y="84" class="m">/ 01 · agent writes files</text>
<path d="M17 92L223 92" class="h"/>
<rect x="17.5" y="104.5" width="205" height="247" class="win"/>
<rect x="18" y="105" width="204" height="20" class="frame"/>
<text x="120" y="118.1" class="m fig" text-anchor="middle">chat</text>
<rect x="18" y="125" width="204" height="226" class="pane"/>
<text x="251" y="84" class="m">/ 02 · drydock compiles</text>
<path d="M251 92L457 92" class="h"/>
<rect x="251.5" y="104.5" width="205" height="247" class="win"/>
<rect x="252" y="105" width="204" height="20" class="frame"/>
<text x="354" y="118.1" class="m fig" text-anchor="middle">drydock</text>
<rect x="252" y="125" width="204" height="226" class="pane"/>
<text x="485" y="84" class="m">/ 03 · agent inspects page</text>
<path d="M485 92L691 92" class="h"/>
<rect x="485.5" y="104.5" width="205" height="247" class="win"/>
<rect x="486" y="105" width="204" height="20" class="frame"/>
<text x="588" y="118.1" class="m fig" text-anchor="middle">prototype</text>
<rect x="486" y="125" width="204" height="226" class="pane"/>
<g class="arrow1"><text x="237" y="232.7" class="m" text-anchor="middle">→</text></g>
<g class="arrow2"><text x="471" y="232.7" class="m" text-anchor="middle">→</text></g>
<rect x="29.5" y="132.5" width="181" height="35" class="tag"/><text x="35" y="146" class="m s10 tt">“a pricing page with three</text><text x="35" y="160" class="m s10 tt">plans + a compare button”</text>
<text x="29" y="188" class="m s10 mute">/ source tree · 4 files</text>
<g class="f1"><rect x="29" y="200.5" width="6" height="6" class="sq"/><text x="41" y="208" class="m">app.tsx</text></g>
<g class="f2"><rect x="29" y="220.5" width="6" height="6" class="sq"/><text x="41" y="228" class="m">plancard.tsx</text></g>
<g class="f3"><rect x="29" y="240.5" width="6" height="6" class="sq"/><text x="41" y="248" class="m">plans.json</text></g>
<g class="f4"><rect x="29" y="260.5" width="6" height="6" class="sq"/><text x="41" y="268" class="m">styles.css</text></g>
<g class="a3"><rect x="18" y="333" width="204" height="18" class="termf"/><path d="M18 333.5H222" class="hc"/><text x="29" y="345.9" class="m s10 light">&gt; agent · edits app.tsx</text></g>
<text x="263" y="146" class="m s10 mute">in the browser · no server</text>
<rect x="263.5" y="156.5" width="181" height="83" class="term"/>
<g class="cl1"><text x="273" y="175" class="m s10 light">&gt; resolve · 4 files</text></g>
<g class="cl2"><text x="273" y="189" class="m s10 light">&gt; compile · tsx → js</text></g>
<g class="cl3"><text x="273" y="203" class="m s10 light">&gt; mount · #root · styles</text></g>
<g class="cl4"><text x="273" y="217" class="m s10 light">&gt; report · no failures</text></g>
<rect class="blink sql" x="273" y="222" width="4" height="8"/>
<text x="263" y="264" class="m s10 mute">/ contract</text>
<rect x="263.5" y="272.5" width="171" height="17" class="tag"/><text x="269" y="285.7" class="m tt">in · files: react + ts</text>
<rect x="263.5" y="298.5" width="151" height="17" class="tag"/><text x="269" y="311.7" class="m tt">out · page + report</text>
<g class="p1"><text x="497" y="148" class="" font-size="14" font-weight="500" letter-spacing="-.02em">Plans</text><path d="M497 154L601 154" class="h"/></g>
<g class="p2"><rect x="497.5" y="164.5" width="181" height="27" class="card"/><text x="505" y="181.9" font-size="10" font-weight="500">Starter</text><text x="671" y="181.9" class="m s10 mute" text-anchor="end">$0</text></g>
<g class="p3"><rect x="497.5" y="200.5" width="181" height="27" class="card"/><text x="505" y="217.9" font-size="10" font-weight="500">Team</text><text x="671" y="217.9" class="m s10 mute" text-anchor="end">$99</text></g>
<g class="p4"><rect x="497.5" y="236.5" width="181" height="27" class="card"/><text x="505" y="253.9" font-size="10" font-weight="500">Enterprise</text><text x="671" y="253.9" class="m s10 mute" text-anchor="end">custom</text></g>
<g class="p4"><rect x="497.5" y="272.5" width="71" height="15" rx="7.5" class="h"/><text x="533" y="283.6" class="" text-anchor="middle" font-size="9">Compare plans</text></g>
<g class="insp"><rect x="497.5" y="296.5" width="135" height="17" class="tag"/><text x="503" y="309.7" class="m tt">viewport · 390 px</text></g>
<g class="rep"><rect x="497.5" y="322.5" width="159" height="17" class="tag lime"/><text x="503" y="335.7" class="m lt">report · no failures</text></g>
<g class="loopa rmh"><path d="M679 352.5V369.5H443.2M264.8 369.5H29V352.5" class="h"/><path d="M26 356.5l3-4 3 4" class="h"/><text x="354" y="372.9" class="m s10 mute" text-anchor="middle">report → the agent iterates</text></g>
<g class="pub"><text x="29" y="373.7" class="m">/ 04 · publish</text><text x="144.416" y="373.7" class="m">→</text><g transform="translate(175,0)"><rect x="0.5" y="360.5" width="163" height="17" class="tag"/><rect x="6" y="366" width="6" height="6" class="sq"/><text x="18" y="373.7" class="m tt">viewer a · compiles</text></g><g transform="translate(351,0)"><rect x="0.5" y="360.5" width="163" height="17" class="tag"/><rect x="6" y="366" width="6" height="6" class="sq"/><text x="18" y="373.7" class="m tt">viewer b · compiles</text></g><g transform="translate(527,0)"><rect x="0.5" y="360.5" width="163" height="17" class="tag"/><rect x="6" y="366" width="6" height="6" class="sq"/><text x="18" y="373.7" class="m tt">viewer c · compiles</text></g></g>
<g class="caps">
<text x="0" y="420" class="m s1 rmh">/ 01 · the agent writes a multi-file source tree from the prompt</text>
<text x="0" y="420" class="m s2 rmh">/ 02 · drydock compiles it in the browser and returns a running page with a report</text>
<text x="0" y="420" class="m s3 rmh">/ 03 · the page renders in the tab; compile and runtime failures land in the report</text>
<text x="0" y="420" class="m s4 rmh">/ 04 · the agent reads the page, switches viewport, screenshots, and edits files again</text>
<text x="0" y="420" class="m s5">/ 05 · on publish, one snapshot is shared; every viewer’s browser compiles that copy fresh</text>
</g></g>
</svg>
<p id="fig-lifecycle-cap" class="text-smallcaps">The agent writes files; Drydock compiles them in the browser; the agent inspects and iterates; viewers compile the copy.</p>

Authors can give permission for others to comment on an element of the page or remix the published prototype into a copy of their own.

As prototypes became inputs to reviews, analyses, and production work, Stripes needed better ways to discuss changing designs and move them between AI tools. In the next sections, we’ll cover how we added commenting in Harbor, and how we integrated Harbor into other AI tooling at Stripe.

### Anchoring comments to changing prototypes

As more Stripes began using Harbor, they wanted to leave comments. Because Harbor prototypes can change, people and agents need to easily understand comments in their right context. We built comments so they could stay connected to what prompted them, and let people and the agent know when that connection has changed.

Anchoring a comment to a page that the agent keeps rewriting is challenging. Harbor solves this by capturing several signals, including semantic labels, stable identifiers, text evidence, structural paths, list keys, and the surrounding context, such as a dialog or accordion, that needs to be opened to reveal the element. It also records whether a signal was unique at the time. When the agent updates the prototype, the resolution flow uses the stored information to report if the comment’s anchor element is attached, needs review, or is orphaned, rather than silently moving the comment to a plausible replacement.

<svg xmlns="http://www.w3.org/2000/svg" id="fig-anchoring" viewBox="0 0 708 400" style="display:block;width:100%;height:auto;margin:24px 0 0" role="img" aria-labelledby="an-t an-d">
<title id="an-t">How Harbor anchors a reviewer’s comment to a card after the agent rewrites the page</title>
<desc id="an-d">A reviewer pins a comment to the Team plan card. Harbor records several signals for the anchor. The agent rewrites the page: cards move, one changes, one is removed. Each comment re-resolves as attached, needs review, or orphaned.</desc>
<defs><pattern id="an-grid" width="20" height="20" patternUnits="userSpaceOnUse"><circle cx="10" cy="10" r="1" style="fill:var(--artGridDotColor,var(--windowBarText,#1e1e1e))" opacity=".2"/></pattern></defs>
<style>
#fig-anchoring-cap{display:none;margin:8px 0 0}
#fig-anchoring text{font-family:'sohne-var','Helvetica Neue',Arial,sans-serif;fill:var(--bodyTextColor,#1e1e1e)}
#fig-anchoring .m{font-family:'sohne-mono',ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:300;letter-spacing:-.012em;font-size:12px;text-transform:uppercase;fill:var(--squareTextColor,#1e1e1e)}
#fig-anchoring .s10{font-size:10px}#fig-anchoring .fig{font-size:8px;fill:var(--windowBarText,#1e1e1e)}#fig-anchoring .bt{fill:var(--windowBarText,#1e1e1e)}
#fig-anchoring .mute{fill-opacity:.55}#fig-anchoring .tt{fill:var(--tagTextColor,#1e1e1e)}#fig-anchoring .lt{fill:var(--invertedTextColor,#1e1e1e)}#fig-anchoring .light{fill:var(--bodyTextColor,#1e1e1e)}
#fig-anchoring .h{fill:none;stroke:var(--artStroke,#1e1e1e);stroke-width:1;shape-rendering:crispEdges}
#fig-anchoring .win{fill:var(--artBackground,transparent);stroke:var(--artStroke,#1e1e1e);stroke-width:1}#fig-anchoring .frame{fill:var(--windowFrameBG,#dcdcdc)}#fig-anchoring .ico{fill:var(--windowBarText,#1e1e1e)}
#fig-anchoring .canvas{stroke:var(--artStroke,#1e1e1e);stroke-width:1}#fig-anchoring .pane{fill:var(--artForeground,#e8e8e8)}
#fig-anchoring .tag{fill:var(--artForeground,#e8e8e8);stroke:var(--tagBorderColor,#1e1e1e44);stroke-width:1;stroke-dasharray:1 1;rx:3}#fig-anchoring .lime{fill:var(--tagHighlightColor,#c4e817)}
#fig-anchoring .term{fill:var(--codeBG,#f5f5f5);stroke:var(--codeBorderColor,#d5d5d5);stroke-width:1;rx:4}#fig-anchoring .termf{fill:var(--codeBG,#f5f5f5)}#fig-anchoring .hc{fill:none;stroke:var(--codeBorderColor,#d5d5d5);stroke-width:1;shape-rendering:crispEdges;vector-effect:non-scaling-stroke}
#fig-anchoring .pinbg{fill:var(--artForeground,#e8e8e8)}#fig-anchoring .sq{fill:var(--artStroke,#1e1e1e)}#fig-anchoring .sql{fill:var(--bodyTextColor,#1e1e1e)}#fig-anchoring .sqo{fill:none;stroke:var(--artStroke,#1e1e1e);stroke-width:1}#fig-anchoring .sqol{fill:none;stroke:var(--bodyTextColor,#1e1e1e);stroke-width:1}#fig-anchoring .sqd{fill:none;stroke:var(--artStroke,#1e1e1e);stroke-width:1;stroke-dasharray:1.5 1.5;stroke-opacity:.6}
#fig-anchoring .card{fill:var(--artForeground,#e8e8e8);stroke:var(--artStroke,#1e1e1e);stroke-width:1;shape-rendering:crispEdges}
#fig-anchoring .dash{fill:none;stroke:var(--artStroke,#1e1e1e);stroke-width:1;stroke-dasharray:3 3}
#fig-anchoring .h,#fig-anchoring .win,#fig-anchoring .canvas,#fig-anchoring .tag,#fig-anchoring .term,#fig-anchoring .card,#fig-anchoring .sqo,#fig-anchoring .sqol,#fig-anchoring .sqd,#fig-anchoring .dash{vector-effect:non-scaling-stroke}
#fig-anchoring *{animation-duration:30s;animation-iteration-count:infinite;animation-timing-function:cubic-bezier(.2,.8,.2,1);animation-fill-mode:both}
#fig-anchoring .scene{animation-name:an-scene}#fig-anchoring .c1t{animation-name:an-c1t}#fig-anchoring .c1n{animation-name:an-c1n}#fig-anchoring .n2{animation-name:an-c1t}#fig-anchoring .n3{animation-name:an-c1n}#fig-anchoring .c2{animation-name:an-c2}#fig-anchoring .c3{animation-name:an-c3}#fig-anchoring .cn{animation-name:an-cn}#fig-anchoring .flk{animation-name:an-flk}#fig-anchoring .con{animation-name:an-con}#fig-anchoring .blink{animation-name:an-blink;animation-duration:1s;animation-timing-function:steps(1)}#fig-anchoring .cur{animation-name:an-cur}#fig-anchoring .pinA{animation-name:an-pinA}#fig-anchoring .trail{animation-name:an-trail}#fig-anchoring .lead{animation-name:an-lead}#fig-anchoring .pS{animation-name:an-pS}#fig-anchoring .d0{animation-name:an-sig0}#fig-anchoring .d1{animation-name:an-sig1}#fig-anchoring .d2{animation-name:an-sig2}#fig-anchoring .d3{animation-name:an-sig3}#fig-anchoring .d4{animation-name:an-sig4}#fig-anchoring .d5{animation-name:an-sig5}#fig-anchoring .d6{animation-name:an-sig6}#fig-anchoring .pR{animation-name:an-pR}#fig-anchoring .rOk{animation-name:an-rOk}#fig-anchoring .pinB{animation-name:an-pinB}#fig-anchoring .rWarn{animation-name:an-rWarn}#fig-anchoring .pinC{animation-name:an-pinC}#fig-anchoring .rOrph{animation-name:an-rOrph}#fig-anchoring .s1{animation-name:an-s1}#fig-anchoring .s2{animation-name:an-s2}#fig-anchoring .s3{animation-name:an-s3}#fig-anchoring .s4{animation-name:an-s4}
@keyframes an-scene{0%{opacity:0}4.44%,94.44%{opacity:1}98.89%,100%{opacity:0}}@keyframes an-c1t{0%,57.22%{opacity:1}58.89%,100%{opacity:0}}@keyframes an-c1n{0%,57.78%{opacity:0}59.44%,100%{opacity:1}}@keyframes an-c2{0%,55%{transform:translate(0,0)}60%,100%{transform:translate(0,52px)}}@keyframes an-c3{0%,52.22%{opacity:1;transform:translate(0,0)}54.44%,100%{opacity:0;transform:translate(8px,0)}}@keyframes an-cn{0%,57.22%{opacity:0;transform:translate(-8px,0)}60.56%,100%{opacity:1;transform:translate(0,0)}}@keyframes an-flk{0%,50.56%{opacity:0}52.22%,57.78%{opacity:.7}60.00%,100%{opacity:0}}@keyframes an-con{0%,47.78%{opacity:0}49.44%,62.22%{opacity:1}63.89%,100%{opacity:0}}@keyframes an-blink{0%,49%{opacity:1}50%,100%{opacity:0}}@keyframes an-cur{0%,6.67%{opacity:0;transform:translate(440px,380px)}8.33%{opacity:1;transform:translate(440px,380px)}13.89%{opacity:1;transform:translate(260px,200px)}16.67%{opacity:1;transform:translate(260px,200px)}18.89%,100%{opacity:0;transform:translate(260px,200px)}}@keyframes an-pinA{0%,13.89%{opacity:0;transform:translate(260px,200px)}16.11%{opacity:1;transform:translate(260px,200px)}55%{opacity:1;transform:translate(260px,200px)}60%,100%{opacity:1;transform:translate(260px,252px)}}@keyframes an-trail{0%,55%{opacity:0;transform:scaleY(0)}55.01%{opacity:1;transform:scaleY(0)}60%,65.56%{opacity:1;transform:scaleY(1)}68.89%,100%{opacity:0;transform:scaleY(1)}}@keyframes an-lead{0%,16.67%{opacity:0}18.89%,46.67%{opacity:1}49.44%,100%{opacity:0}}@keyframes an-pS{0%,25.56%{opacity:0}27.78%,46.67%{opacity:1}49.44%,100%{opacity:0}}@keyframes an-sig0{0%,27.78%{opacity:0;transform:translateX(-4px)}30.00%,100%{opacity:1;transform:none}}@keyframes an-sig1{0%,29.45%{opacity:0;transform:translateX(-4px)}31.67%,100%{opacity:1;transform:none}}@keyframes an-sig2{0%,31.11%{opacity:0;transform:translateX(-4px)}33.33%,100%{opacity:1;transform:none}}@keyframes an-sig3{0%,32.78%{opacity:0;transform:translateX(-4px)}35.00%,100%{opacity:1;transform:none}}@keyframes an-sig4{0%,34.45%{opacity:0;transform:translateX(-4px)}36.67%,100%{opacity:1;transform:none}}@keyframes an-sig5{0%,36.11%{opacity:0;transform:translateX(-4px)}38.33%,100%{opacity:1;transform:none}}@keyframes an-sig6{0%,37.78%{opacity:0;transform:translateX(-4px)}40.00%,100%{opacity:1;transform:none}}@keyframes an-pR{0%,65.56%{opacity:0}67.78%,100%{opacity:1}}@keyframes an-rOk{0%,68.89%{opacity:0}71.11%,100%{opacity:1}}@keyframes an-pinB{0%,74.44%{opacity:0;transform:translate(260px,148px)}76.67%,100%{opacity:1;transform:translate(260px,148px)}}@keyframes an-rWarn{0%,75.56%{opacity:0}77.78%,100%{opacity:1}}@keyframes an-pinC{0%,82.78%{opacity:0;transform:translate(260px,284px)}85.00%{opacity:1;transform:translate(260px,284px)}88.33%,100%{opacity:.6;transform:translate(260px,284px)}}@keyframes an-rOrph{0%,81.11%{opacity:0}83.33%,100%{opacity:1}}@keyframes an-s1{0%,4.44%{opacity:0}6.67%,23.33%{opacity:1}25.56%,100%{opacity:0}}@keyframes an-s2{0%,25.56%{opacity:0}27.78%,46.67%{opacity:1}48.89%,100%{opacity:0}}@keyframes an-s3{0%,47.78%{opacity:0}50.00%,63.33%{opacity:1}65.56%,100%{opacity:0}}@keyframes an-s4{0%,65.56%{opacity:0}67.78%,100%{opacity:1}}
@media (prefers-reduced-motion:reduce){#fig-anchoring *{animation:none !important}#fig-anchoring .rmh{display:none}#fig-anchoring .c2{transform:translate(0,52px)}#fig-anchoring .rms{display:inline}}
@media (max-width:600px){#fig-anchoring .caps,#fig-anchoring .mob-hide{display:none}#fig-anchoring-cap{display:block;font-family:'sohne-mono',ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;font-weight:300;letter-spacing:-.012em;text-transform:uppercase;line-height:1.4;color:var(--squareTextColor,#1e1e1e)}}
</style>
<text x="0" y="16" class="m">/ comment anchoring</text>
<path d="M0 24L708 24" class="h"/>
<g class="scene">
<rect x="0.5" y="36.5" width="707" height="331" class="win"/>
<rect x="1" y="37" width="706" height="20" class="frame"/>
<path transform="translate(5,41)" d="M11 0H1V1H0V11H1V12H11V11H12V1H11V0ZM11 1V11H1V1H11ZM6 7H10V8H6V7ZM3 7H2V8H3V7ZM4 6V7H3V6H4ZM4 5V6H5V5H4ZM3 4H4V5H3V4ZM3 4V3H2V4H3Z" class="ico" fill-rule="evenodd"/>
<path transform="translate(691,41)" d="M-8.74228e-08 10L8 10L8 9L11 9L11 10L12 10L12 11L11 11L11 12L8 12L8 11L-4.37113e-08 11L-8.74228e-08 10ZM-3.93402e-07 3L-4.37114e-07 2L8 2L8 1L11 1L11 2L12 2L12 3L11 3L11 4L8 4L8 3L-3.93402e-07 3ZM1 6L-2.62268e-07 6L-2.18557e-07 7L1 7L1 8L4 8L4 7L12 7L12 6L4 6L4 5L1 5L1 6ZM9 2L10 2L10 3L9 3L9 2ZM10 10L9 10L9 11L10 11L10 10ZM2 6L3 6L3 7L2 7L2 6Z" class="ico" fill-rule="evenodd"/>
<text x="354" y="50.1" class="m fig" text-anchor="middle">[ fig. 2 ]</text>
<rect x="1" y="57" width="4" height="310" class="frame"/>
<rect x="703" y="57" width="4" height="310" class="frame"/>
<rect x="1" y="363" width="706" height="4" class="frame"/>
<rect x="5.5" y="57.5" width="697" height="305" rx="2" fill="url(#an-grid)" class="canvas"/>
<rect x="17.5" y="69.5" width="474" height="282" class="win"/>
<rect x="18" y="70" width="473" height="20" class="frame"/>
<text x="254.5" y="83.1" class="m fig" text-anchor="middle">prototype · published</text>
<rect x="18" y="90" width="473" height="261" class="pane"/>
<text x="29" y="114" class="" font-size="20" font-weight="500" letter-spacing="-.02em">Plans</text>
<path d="M29 120L219 120" class="h"/>
<g><rect x="29.5" y="128.5" width="450" height="39" class="card"/><text x="41" y="145" font-size="14" font-weight="500"><tspan class="c1t">Starter</tspan><tspan class="c1n" x="41">Starter Plus</tspan></text><text x="41" y="159" class="m s10 mute">individuals · $0 / month</text><text x="468" y="145" class="m mute" text-anchor="end">01</text></g>
<g class="c2"><rect x="29.5" y="180.5" width="450" height="39" class="card"/><text x="41" y="197" font-size="14" font-weight="500">Team</text><text x="41" y="211" class="m s10 mute">growing teams · $99 / month</text><text x="468" y="197" text-anchor="end" class="m mute"><tspan class="n2 rmh">02</tspan><tspan class="n3" x="468">03</tspan></text></g>
<g class="c3 rmh"><rect x="29.5" y="232.5" width="450" height="39" class="card"/><text x="41" y="249" font-size="14" font-weight="500">Enterprise</text><text x="41" y="263" class="m s10 mute">custom · sso · dedicated support</text><text x="468" y="249" class="m mute" text-anchor="end">03</text></g>
<g class="cn"><rect x="29.5" y="180.5" width="450" height="39" class="card"/><text x="41" y="197" font-size="14" font-weight="500">Growth</text><text x="41" y="211" class="m s10 mute">small teams · $49 / month</text><text x="468" y="197" class="m mute" text-anchor="end">02</text></g>
<g class="rOrph"><text x="29" y="288" class="m s10 mute">enterprise · removed by the rewrite</text></g>
<rect x="29.5" y="300.5" width="95" height="19" rx="9.5" class="h"/><text x="77" y="313.9" class="" text-anchor="middle" font-size="10">Compare plans</text>
<rect x="25.5" y="124.5" width="458" height="151" class="dash flk rmh"/>
<g class="con rmh"><rect x="18" y="333" width="473" height="18" class="termf"/><path d="M18 333.5H491" class="hc"/><text x="29" y="345.9" class="m s10 light">&gt; agent · rewriting the page</text><rect class="blink light sql" x="205" y="337" width="4" height="10"/></g>
<g class="cur rmh"><path d="M0 0v12l3-3h5z" class="sq"/></g>
<g class="trail rmh" style="transform-origin:260px 206px"><path d="M260 206V246" class="h" stroke-dasharray="2 2"/></g>
<g class="pinA" transform="translate(260,252.0)"><rect x="-6" y="-6" width="12" height="12" class="pinbg"/><rect x="-5.5" y="-5.5" width="11" height="11" class="sqo"/><rect x="-3" y="-3" width="6" height="6" class="sq"/></g>
<g class="pinB" transform="translate(260,148.0)"><rect x="-6" y="-6" width="12" height="12" class="pinbg"/><rect x="-5.5" y="-5.5" width="11" height="11" class="sqo"/><rect x="-2.5" y="-2.5" width="5" height="5" class="sqo"/></g>
<g class="pinC" transform="translate(260,284)"><rect x="-6" y="-6" width="12" height="12" class="pinbg"/><rect x="-5.5" y="-5.5" width="11" height="11" class="sqd"/></g>
<g class="panel">
<g class="pR"><text x="504" y="84" class="m">/ resolution</text><path d="M504 92L691 92" class="h"/></g>
<g class="pS rmh"><text x="504" y="84" class="m">/ signals recorded</text><path d="M504 92L691 92" class="h"/>
<g class="lead rmh"><rect x="504.5" y="104.5" width="179" height="17" class="tag"/><rect x="510" y="110" width="6" height="6" class="sq"/><text x="522" y="117.7" class="m tt">comment · “price ok?”</text></g>
<g class="sig d0 rmh"><rect x="504.5" y="134.5" width="171" height="17" class="tag"/><rect x="510" y="140" width="6" height="6" class="sq"/><text x="522" y="147.7" class="m tt">label · card:pricing</text></g>
<g class="sig d1 rmh"><rect x="504.5" y="160.5" width="127" height="17" class="tag"/><rect x="510" y="166" width="6" height="6" class="sq"/><text x="522" y="173.7" class="m tt">id · plan-team</text></g>
<g class="sig d2 rmh"><rect x="504.5" y="186.5" width="119" height="17" class="tag"/><rect x="510" y="192" width="6" height="6" class="sq"/><text x="522" y="199.7" class="m tt">text · “team”</text></g>
<g class="sig d3 rmh"><rect x="504.5" y="212.5" width="171" height="17" class="tag"/><rect x="510.5" y="218.5" width="5" height="5" class="sqo"/><text x="522" y="225.7" class="m tt">path · main/ul/li[2]</text></g>
<g class="sig d4 rmh"><rect x="504.5" y="238.5" width="135" height="17" class="tag"/><rect x="510" y="244" width="6" height="6" class="sq"/><text x="522" y="251.7" class="m tt">list key · team</text></g>
<g class="sig d5 rmh"><rect x="504.5" y="264.5" width="119" height="17" class="tag"/><rect x="510.5" y="270.5" width="5" height="5" class="sqo"/><text x="522" y="277.7" class="m tt">reveal · none</text></g>
<g class="sig d6 rmh"><rect x="504" y="297" width="6" height="6" class="sq"/><text x="516" y="303.9" class="m s10 mute">unique</text><rect x="568.5" y="297.5" width="5" height="5" class="sqo"/><text x="580" y="303.9" class="m s10 mute">not unique</text></g></g>
<g class="rWarn"><rect x="504.5" y="139.5" width="175" height="17" class="term"/><rect x="510.5" y="145.5" width="5" height="5" class="sqol"/><text x="522" y="152.7" class="m light">review · text changed</text></g>
<g class="rOk"><rect x="504.5" y="243.5" width="175" height="17" class="tag lime"/><rect x="510" y="249" width="6" height="6" class="sq"/><text x="522" y="256.7" class="m lt">attached · card moved</text></g>
<g class="rOrph"><rect x="504.5" y="279.5" width="155" height="17" class="tag"/><rect x="510.5" y="285.5" width="5" height="5" class="sqd"/><text x="522" y="292.7" class="m tt">orphaned · removed</text></g>
</g>
<g class="caps">
<text x="0" y="392" class="m s1 rmh">/ 01 · a reviewer pins a comment to the team card</text>
<text x="0" y="392" class="m s2 rmh">/ 02 · harbor records several signals for the anchor, and whether each was unique</text>
<text x="0" y="392" class="m s3 rmh">/ 03 · the agent rewrites the page: cards move, one changes, one is removed</text>
<text x="0" y="392" class="m s4">/ 04 · comments re-resolve: attached, needs review, or orphaned. nothing is silently moved</text>
</g></g>
</svg>
<p id="fig-anchoring-cap" class="text-smallcaps">A comment is pinned; the agent rewrites the page; it re-resolves: attached, needs review, or orphaned.</p>

Published comments are tied to the version of the prototype where they were posted. When the author publishes a new snapshot, Harbor marks existing comments as stale so reviewers can tell which version the feedback is for.

### Integrating Harbor with other AI tools

Harbor’s in-browser experience worked well for designers, but Stripes working in our other AI tools wanted to access it from there, so they could bring interactive visual prototypes into existing workflows. Two architectural choices made this possible: exposing Harbor’s toolset over our internal Model Context Protocol (MCP) server, and exposing Drydock, Harbor’s rendering engine, as its own service.

Harbor exposes a toolset through Stripe’s MCP infrastructure that allows other agents to create, read, update, and publish a prototype without needing a person to manually copy context into a new conversation. The same toolset is available to coding agents working on Stripe devboxes. They can pull a Harbor prototype’s file tree into a real repository, or push an idea from another workflow into Harbor for visual exploration. The handoff preserves the decisions around ownership and implementation while eliminating manual transcription between tools.

Drydock, Harbor’s rendering engine, has a small contract: it takes a multi-file React and TypeScript file tree and returns a running page inside the browser, alongside a report that includes compile and runtime failures, so that the agent can inspect and interact with the running result. Because that contract doesn’t depend on anything else in Harbor, other tools across Stripe can use it. This was particularly useful in Kai, our AI platform for knowledge work. Harbor gave Kai users the means to take the next step with a prototype. One of our producers, who run stripe.com programs across design, engineering, and editorial, ran a viewport analysis of [stripe.com](https://stripe.com) in Kai, then used Harbor to turn the results into a clickable prototype.

## What we learned

Harbor began as an experiment in building with AI. Now it gives all Stripes a way to rapidly experiment and iterate on visual prototypes. We started with solving a small, team-focused problem and expanded beyond its original boundary thanks to our [culture of friction logging](https://mikebifulco.com/posts/how-stripe-uses-friction-logs), AI-forward culture, and colleagues who were willing to try out the early version of a tool, share what they made, and help develop useful pieces into shared infrastructure.

In Stripe’s internal AI tools, we’ve found that it’s effective to keep agents specialized (on research, coding, or browser-native tools) while exposing them to capabilities in other systems. In our viewport analysis example, we started with research in Kai, and built an interactive prototype in Harbor. In other examples, engineers mocked up internal admin tools in Harbor, then handed the generated prototype to a coding agent to rebuild for production. And Harbor’s own rendering engine became infrastructure for Kai.

## What’s next

Our goal is to continue to make Stripe’s AI tools work together rather than have each tool solve every part of the workflow. For Harbor, we want prototypes to hold small amounts of state and connect to live data. That includes prototype-scoped databases and embeddable widgets that refresh internal data queries on their own, instead of requiring an agent to copy numbers into the source and rewrite them whenever the data changes.

We’re still early in understanding what becomes possible when research, code, and interactive artifacts can move between AI tools without losing their context. There’s a lot left to build, and the work spans infrastructure, developer tools, product systems, and AI all at once. If that’s the kind of problem you want to work on, [we’re hiring](https://grnh.se/b5wzjpge1us).
