<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Building @Owner</title>
    <link>https://building.owner.com/</link>
    <description>Notes from the engineers building Owner, the AI platform restaurants use to grow first-party sales.</description>
    <language>en-us</language>
    <atom:link href="https://building.owner.com/rss.xml" rel="self" type="application/rss+xml" />
    <image><url>https://building.owner.com/images/apple-touch-icon.png</url><title>Building @Owner</title><link>https://building.owner.com/</link></image>
    <lastBuildDate>Tue, 13 Oct 2026 12:00:00 GMT</lastBuildDate>
    <item>
      <title>Project Speed</title>
      <link>https://building.owner.com/project-speed</link>
      <guid isPermaLink="true">https://building.owner.com/project-speed</guid>
      <pubDate>Tue, 13 Oct 2026 12:00:00 GMT</pubDate>
      <dc:creator>Ryan Selden</dc:creator>
      <dc:creator>Jack Hanford</dc:creator>
      <category>Code</category>
      <description>How Owner replaced Next.js with Astro and Cloudflare Workers to speed 10,000+ restaurant websites, improve conversion, and raise per-restaurant revenue by 4%.</description>
      <content:encoded><![CDATA[<p>Big restaurant chains invest millions into fast, high-converting websites. That is one reason your local pizzeria struggles to compete against Domino’s. Now that Owner has scaled to 10K+ restaurant websites, we have enough traffic to test changes and measure their effect on conversion and sales, then ship the successful variants to every business. Project Speed was one of those changes. We rebuilt our website infrastructure for speed, and it raised per-restaurant revenue by more than 4% across the network.</p>
<p>We replaced our Next.js rendering stack, codenamed Mercury, with an Astro stack we call Apollo. Apollo serves cached pages from the edge and only uses React where needed. As one measure, largest contentful paint on menu pages went from 5.4 seconds to 0.7 seconds at the 75th percentile.</p>
<p>In July 2025, I was inspired by <a href="https://www.mcmaster.com/">McMaster-Carr</a> (I’m a frequent customer) and thought it would be cool if Owner websites were that fast. Two months later Jack joined the team and we were off to the races. By late January 2026, Apollo served 100% of our traffic.</p>
<iframe class="rollout-embed" src="https://building.owner.com/embeds/project-speed-rollout" title="Project Speed rollout: from Mercury to Apollo" width="940" height="800" loading="lazy"></iframe>
<h2 id="why-mercury-was-slow">Why Mercury was slow</h2>
<p>When Mercury needed to render a page, it fetched data from our backend, Olympus, then rendered the page and sent the response to the user. That round trip is slow. So we cached. Then we cached the caches. That somewhat worked, and most of the time the websites looked reasonably fast. But pages had unpredictable delays, website changes took a while to publish, and every time we redeployed it cleared the cache.</p>
<p>Mercury also loaded more data than most pages needed, due to inherent design choices when the largest customer we served had one menu and two locations. We loaded every location for a brand and every menu, including items, categories, and modifiers. Our platform lead Dmitry winced every time we saw massively oversized assets, slow loading times, and request amplification issues at the backend.</p>
<p>Third-party scripts added more overhead. Google Tag Manager and the Facebook pixel loaded on every page. We couldn’t remove them without breaking customers’ analytics, and they affected our lab scores. Our baseline and targets:</p>
<div class="table-wrap" role="region" aria-label="Performance baseline and targets" tabindex="0"><table>
<thead><tr>
<th scope="col">Metric (p75)</th>
<th scope="col">Before</th>
<th scope="col">Target</th>
</tr></thead><tbody>
<tr>
<td>Largest contentful paint</td>
<td>5 to 6s</td>
<td>under 1s</td>
</tr>
<tr>
<td>Time to first byte</td>
<td>600ms+</td>
<td>under 200ms</td>
</tr>
<tr>
<td>Interaction to next paint</td>
<td>~200ms</td>
<td>under 100ms</td>
</tr>
<tr>
<td>JavaScript bundle</td>
<td>~1,500KB</td>
<td>under 100KB marketing, under 250KB dynamic</td>
</tr>
<tr>
<td>Google PageSpeed (out of 100)</td>
<td>~45</td>
<td>90+</td>
</tr>
</tbody></table></div>
<h2 id="why-we-chose-astro">Why we chose Astro</h2>
<p>We wanted a framework that defaulted to markup-only components and let us choose where to add client-side JavaScript. Most of our website content didn’t need React running in the browser. We also considered Remix, TanStack Start, and Gatsby. We chose Astro for 4 reasons:</p>
<ol>
<li>It renders components to markup by default; client-side framework JavaScript is opt-in.</li>
<li>It supports islands: self-contained components with their own hydration boundaries, using React or another supported framework.</li>
<li>It supports prerendered and on-demand pages in the same app, with client-side interactivity where needed.</li>
<li>It integrates deeply with Cloudflare, a platform we wanted to bet on.</li>
</ol>
<h2 id="architecture">Architecture</h2>
<p>Apollo renders pages to markup and caches the output. Cache hits serve that markup. We refresh the affected pages in the background when restaurant info changes.</p>
<p>The interactive parts are React islands. Astro renders the whole page to markup on the server, then hydrates the islands only, each on its own schedule. The rest of the page doesn't need any client-side React.</p>
<p>Marketing pages (careers, FAQ, location, about) don’t need much JavaScript. The menu page puts the ordering flow in a single React island, keeping the ordering experience inside one hydration boundary. Over time, we'll make more of the menu experience static to drive performance gains further without sacrificing UX.</p>
<p>Client directives control when Astro loads and hydrates an island:</p>
<ul>
<li><code>client:load</code> loads and hydrates on page load. We use this for the menu and cart because a user might tap “add to order” immediately.</li>
<li><code>client:visible</code> loads and hydrates when the component enters the viewport.</li>
<li><code>client:idle</code> defers loading and hydration until after the initial page load, using the browser’s idle callback where supported.</li>
</ul>
<p>We didn't want to rebuild the whole online ordering flow at first. All we needed to get that experience working in Astro was roughly:</p>
<pre class="astro-code owner" style="background-color:#0d1e10;color:#f1f6f3; overflow-x: auto;" tabindex="0" data-language="astro"><code><span class="line"><span style="color:#90A594">---</span></span>
<span class="line"><span style="color:#8CCE80">const</span><span style="color:#F1F6F3"> brand </span><span style="color:#8CCE80">=</span><span style="color:#8CCE80"> await</span><span style="color:#B3D9B3"> fetchBrand</span><span style="color:#F1F6F3">(Astro</span><span style="color:#AEBCB2">,</span><span style="color:#F1F6F3"> brandId)</span></span>
<span class="line"><span style="color:#8CCE80">const</span><span style="color:#F1F6F3"> initialPage </span><span style="color:#8CCE80">=</span><span style="color:#8CCE80"> await</span><span style="color:#B3D9B3"> fetchPage</span><span style="color:#F1F6F3">(Astro</span><span style="color:#AEBCB2">,</span><span style="color:#F1F6F3"> brandId</span><span style="color:#AEBCB2">,</span><span style="color:#AEBCB2"> '</span><span style="color:#D5E8C4">/menu</span><span style="color:#AEBCB2">'</span><span style="color:#F1F6F3">)</span></span>
<span class="line"><span style="color:#8CCE80">const</span><span style="color:#F1F6F3"> pageData </span><span style="color:#8CCE80">=</span><span style="color:#8CCE80"> await</span><span style="color:#B3D9B3"> transformPageData</span><span style="color:#F1F6F3">(initialPage</span><span style="color:#AEBCB2">,</span><span style="color:#F1F6F3"> brand)</span></span>
<span class="line"><span style="color:#90A594">---</span></span>
<span class="line"></span>
<span class="line"><span style="color:#AEBCB2">&#x3C;</span><span style="color:#8CDE84">BrandLayout</span><span style="color:#E5C994"> brand</span><span style="color:#AEBCB2">={</span><span style="color:#F1F6F3">brand</span><span style="color:#AEBCB2">}></span></span>
<span class="line"><span style="color:#AEBCB2">  &#x3C;</span><span style="color:#8CDE84">MenuIsland</span></span>
<span class="line"><span style="color:#E5C994">    client:load</span></span>
<span class="line"><span style="color:#E5C994">    brandId</span><span style="color:#AEBCB2">={</span><span style="color:#F1F6F3">brandId</span><span style="color:#AEBCB2">}</span></span>
<span class="line"><span style="color:#E5C994">    pageData</span><span style="color:#AEBCB2">={</span><span style="color:#F1F6F3">pageData</span><span style="color:#AEBCB2">}</span></span>
<span class="line"><span style="color:#AEBCB2">  /></span></span>
<span class="line"><span style="color:#AEBCB2">&#x3C;/</span><span style="color:#8CDE84">BrandLayout</span><span style="color:#AEBCB2">></span></span></code></pre>
<p>The code above the template runs on the server to prepare the page data. Astro renders <code>&lt;MenuIsland&gt;</code> to markup. The <code>client:load</code> directive tells Astro to load the island’s JavaScript and hydrate it on page load. That island contains the entire ordering experience.</p>
<p>We also configured the build to reduce asset requests:</p>
<pre class="astro-code owner" style="background-color:#0d1e10;color:#f1f6f3; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#8CCE80">export</span><span style="color:#8CCE80"> default</span><span style="color:#B3D9B3"> defineConfig</span><span style="color:#F1F6F3">(</span><span style="color:#AEBCB2">{</span></span>
<span class="line"><span style="color:#F1F6F3">  output</span><span style="color:#AEBCB2">:</span><span style="color:#AEBCB2"> '</span><span style="color:#D5E8C4">server</span><span style="color:#AEBCB2">'</span><span style="color:#AEBCB2">,</span></span>
<span class="line"><span style="color:#F1F6F3">  integrations</span><span style="color:#AEBCB2">:</span><span style="color:#F1F6F3"> [</span><span style="color:#B3D9B3">react</span><span style="color:#F1F6F3">()]</span><span style="color:#AEBCB2">,</span></span>
<span class="line"><span style="color:#F1F6F3">  prefetch</span><span style="color:#AEBCB2">:</span><span style="color:#AEBCB2"> {</span><span style="color:#F1F6F3"> prefetchAll</span><span style="color:#AEBCB2">:</span><span style="color:#E7D9A3"> true</span><span style="color:#AEBCB2"> },</span></span>
<span class="line"><span style="color:#F1F6F3">  build</span><span style="color:#AEBCB2">:</span><span style="color:#AEBCB2"> {</span></span>
<span class="line"><span style="color:#F1F6F3">    inlineStylesheets</span><span style="color:#AEBCB2">:</span><span style="color:#AEBCB2"> '</span><span style="color:#D5E8C4">always</span><span style="color:#AEBCB2">'</span><span style="color:#AEBCB2">,</span><span style="color:#90A594"> // no render-blocking CSS requests</span></span>
<span class="line"><span style="color:#F1F6F3">    assetsInlineLimit</span><span style="color:#AEBCB2">:</span><span style="color:#E7D9A3"> 150000</span><span style="color:#AEBCB2">,</span><span style="color:#90A594">    // inline anything under 150KB</span></span>
<span class="line"><span style="color:#AEBCB2">  },</span></span>
<span class="line"><span style="color:#AEBCB2">}</span><span style="color:#F1F6F3">)</span></span></code></pre>
<p>Inlining project CSS removes separate stylesheet requests. Inlining eligible assets removes requests for those files too. We also enable prefetching for links. Some of these settings are unorthodox; we tweaked them with a lot of data to prove this is the best for our app.</p>
<p>Our old setup ran on AWS in <code>us-east-1</code> only. Requests that reached that server traveled there and back, even when it returned cached markup. Apollo runs on Cloudflare Workers across Cloudflare’s edge network. Workers execute our code in Cloudflare’s data centers, letting us render and serve pages near the user. We use Cloudflare's <code>workerd</code> runtime in local development too.</p>
<p>We even moved the brand lookup (figuring out which restaurant a domain belongs to) into the edge worker. That one change shaved about 50ms off time to first byte. Images have their own edge treatment through a service we call Pluto, which resizes and re-encodes photos at the edge.</p>
<h2 id="cache-layer">Cache layer</h2>
<p>Mercury slowed down after every deploy while its caches rebuilt. Apollo needed to avoid that slowdown and limit requests to Olympus. To fix it we built a custom cache, layered like this:</p>
<ol>
<li>Every request first goes through a gateway Worker, which checks feature flags, selects the route type, and determines which markup to serve.</li>
<li>The gateway then forwards the request through <a href="https://blog.cloudflare.com/workers-cache/">Cloudflare’s Workers Cache</a>, placing a tiered cache between the gateway and the rest of our stack.</li>
<li>On a cache miss, we fall back to R2, Cloudflare’s object storage, where HTML is stored alongside metadata such as the version that rendered it and when it was generated. We serve the stored HTML and use that metadata to decide whether to trigger a background refresh of both R2 and the tiered cache.</li>
<li>If no HTML exists in R2, we fall back to the Astro renderer, which generates the page, stores it in R2 with its metadata, and populates the cache.</li>
</ol>
<p><strong>Stale while revalidate.</strong> When we can serve a stale cached page, we return it and refresh it in the background - those requests don’t wait for a re-render.</p>
<p><strong>Tracing requests.</strong> We tag every response with a header (<code>x-owner-source</code>) that says whether it came from the edge cache or a fresh render, and we emit <code>Server-Timing</code> headers for each hop (R2 read, Astro render, R2 write). These headers show where the response came from and how long each measured operation took.</p>
<p><strong>Push, don’t poll.</strong> Olympus calls Apollo’s revalidation endpoint when a restaurant’s data changes, and then Apollo re-renders the affected pages in the background. This reduces repeated fetches from Olympus.</p>
<p>Small table of route types:</p>
<div class="table-wrap" role="region" aria-label="Cache durations by route" tabindex="0"><table>
<thead><tr>
<th scope="col">Route type</th>
<th scope="col">Browser cache</th>
<th scope="col">CDN cache</th>
</tr></thead><tbody>
<tr>
<td>Static assets</td>
<td>30 days (immutable)</td>
<td>30 days</td>
</tr>
<tr>
<td>Marketing pages</td>
<td>60s</td>
<td>600s</td>
</tr>
<tr>
<td>Menu</td>
<td>60s</td>
<td>120s</td>
</tr>
</tbody></table></div>
<p>Cache keys include the brand and a hash of the active feature flags, keeping cached markup separate for each experiment variant. Any page tied to a specific logged-in user (checkout, order status, login) doesn’t go through the shared cache.</p>
<p>By the time we rolled Apollo out to all customers, our markup store had grown to 5TB.</p>
<h2 id="speed-bumps">Speed bumps</h2>
<p>Experiment variants. Each independent binary feature flag doubles the number of cached variants. Many flags could explode into thousands of stored copies and prevent us from properly prerendering and caching. So we bucket groups of customers together at random and run experiments across different segments.</p>
<p>Duplicate React copies. Node loaded 2 copies of React during server rendering. Astro’s deduplication settings didn’t cover that module-loading path. We used a filesystem symlink so Apollo and the root project resolved React to the same copy.</p>
<p>Early Hints. We enabled HTTP 103 Early Hints so the browser could start fetching the hero image before the markup response. Some pages reached a largest contentful paint of about 600ms with Early Hints enabled.</p>
<p>We found so many other performance and optimization wins as we went along. We're still finding them: agents make the pursuit of performance excellence a game we don't know that we'll ever stop playing (and enjoying!).</p>
<h2 id="shipping-it-without-anyone-noticing">Shipping it without anyone noticing</h2>
<p>We ran Apollo and Mercury side by side as a 50/50 experiment measured in PostHog. Some brands stayed on Mercury, others went fully to Apollo, and for the rest, we split their traffic down the middle to compare the stacks on the same kinds of sites.</p>
<p>Jack did such a good job making the new sites pixel perfect to the old, I couldn't tell them apart. We added a little animated Owner logo in the footer (this was pre our redesign).</p>
<figure class="original-footer astro-qr6boxuu" aria-label="Original Made with Owner footer">
  <div class="owner-badge astro-qr6boxuu">
    <span class="astro-qr6boxuu">Made with Owner</span>
    <svg width="30" height="30" viewBox="0 0 204 204" fill="none" aria-hidden="true" class="astro-qr6boxuu">
      <path d="M102 204C158.333 204 204 158.333 204 102C204 45.667 158.333 0 102 0C45.667 0 0 45.667 0 102C0 158.333 45.667 204 102 204Z" fill="black" class="astro-qr6boxuu"></path>
      <g class="pin astro-qr6boxuu">
        <path fill-rule="evenodd" clip-rule="evenodd" d="M102 166C102 166 153 139.4 153 102C153 73.8335 130.167 51 102 51C73.8335 51 51 73.8335 51 102C51 139.4 102 166 102 166Z" fill="white" class="astro-qr6boxuu"></path>
        <circle cx="102" cy="102" r="24" fill="black" class="astro-qr6boxuu"></circle>
      </g>
    </svg>
  </div>
</figure>

<script type="module" src="https://building.owner.com/home/runner/work/epd-blog/epd-blog/src/components/OriginalOwnerFooter.astro?astro&type=script&index=0&lang.ts"></script>
<p>For testing we shipped a small browser extension Sphinx that let an engineer force their own browser onto Apollo or Mercury on any site. Today it powers our feature flag system.</p>
<p>We staged the rollout in steps: 8 brands, then 800, then 4,800, then all. At each step we watched the field data, and we had a kill switch in case we needed to drop back to 50/50 or turn Apollo off entirely.</p>
<p>Timeline:</p>
<ul>
<li>July 2025: Ryan is asked what a big moonshot would be, writes a short doc called "Project Speed"</li>
<li>Oct 2025: Validated Cloudflare Workers</li>
<li>Nov 13, 2025: First real end-to-end order in Apollo</li>
<li>Dec 2025: First brands go live. Scaled to ~4,800 by Christmas.</li>
<li>Jan 2, 2026: All brands enter 50/50 test</li>
<li>Jan 26, 2026: Apollo serves 100% of traffic</li>
<li>Feb 9, 2026: Public launch</li>
</ul>
<h2 id="results">Results</h2>
<ul>
<li>
<p>p75 largest-contentful-paint on the menu page (field RUM): <strong>5.4s</strong> before → <strong>0.7s</strong> after. About 87% faster. We score mobile against a 5-year-old $200 Android phone. Some sites with a fast hero image hit a 0.6-second paint.</p>
</li>
<li>
<p><strong>+23%</strong> click-through from homepage to menu item</p>
</li>
<li>
<p><strong>+4%</strong> per-restaurant revenue</p>
</li>
<li>
<p><strong>$8,000 to $10,000 per month</strong> infrastructure savings</p>
</li>
<li>
<p>Mobile Lighthouse: 90 to 99 (previously 44)</p>
</li>
<li>
<p>Desktop Lighthouse: 98 to 100</p>
</li>
<li>
<p>Page load, lab: &lt;1s</p>
</li>
<li>
<p>In the database, Apollo carts converted about 10% better from "add item" to checkout. One restaurant, El Vaquero, converted 33% better on the new stack (they have a large menu, so the optimizations benefitted them even more).</p>
</li>
</ul>
<figure>
  <img width="1600" height="635" decoding="async" src="https://building.owner.com/images/posts/project-speed-psi-mobile.webp" alt="PageSpeed Insights mobile report for ashleys.cafe: Performance 96, Accessibility 100, Best Practices 100, SEO 100" loading="lazy">
  <figcaption>PageSpeed Insights, mobile: ashleys.cafe</figcaption>
</figure>
<figure>
  <img width="1600" height="585" decoding="async" src="https://building.owner.com/images/posts/project-speed-psi-desktop.webp" alt="PageSpeed Insights desktop report for ashleys.cafe: Performance 100, Accessibility 100, Best Practices 100, SEO 100" loading="lazy">
  <figcaption>PageSpeed Insights, desktop: ashleys.cafe</figcaption>
</figure>
<p>Apollo is how we build our websites now. Since the end of Project Speed, we’ve rebuilt our Restaurant Grader and other frontends in a similar way, following all the lessons we learned.</p>
<p>Owner wants to help local businesses succeed. This is just the start of the work we're doing, and AI is accelerating the pace we can improve our platform for our customers. If you're excited about local restaurants and this sounds like something you want to work on, <a href="https://www.owner.com/careers#roles" target="_blank" rel="noopener noreferrer">reach out to us</a>.</p>]]></content:encoded>
      <enclosure url="https://building.owner.com/og/project-speed.png?v=88ce6bb3" type="image/png" length="0" />
    </item>
    <item>
      <title>Owner Agent, Part 1</title>
      <link>https://building.owner.com/agent-1</link>
      <guid isPermaLink="true">https://building.owner.com/agent-1</guid>
      <pubDate>Mon, 12 Oct 2026 12:00:00 GMT</pubDate>
      <dc:creator>Daniel Ternyak</dc:creator>
      <category>AI</category>
      <description>How Owner turned 224,000 support cases into about 2,000 agent operations, and the channels, tools, approvals, and evals behind Owner Agent.</description>
      <content:encoded><![CDATA[<p>
  Imagine Joe, a local restaurant owner running three locations. Joe is driving from one location to
  another on a Saturday afternoon when he realizes that the third location needs to close early for
  a private event.
</p>
<p>
  The setting is in the Owner mobile app, just 3 clicks deep. But finding it right now feels risky,
  so he calls our team.
</p>
<p>The call goes well. Our team answers the median call in 75 seconds.</p>
<p>
  But the customer found it easier to wait on hold than to use the setting in the dashboard, even
  though the change itself took 3 seconds to make.
</p>
<p>
  In the near future, our customers will know they can easily ask Owner Agent to handle changes like
  this, even by phone - and to do much more besides.
</p>
<figure>
  
<video class="astro-yngxbivw" src="https://building.owner.com/videos/owner-app-hours.mp4" data-poster="https://building.owner.com/videos/owner-app-hours.jpg" width="1600" height="900" muted loop playsinline preload="none" aria-label="Changing business hours with Owner Agent" data-video="owner-app-hours"></video>
  <figcaption>One sentence, one approval card</figcaption>
</figure>

<div class="stats">
  <div><b>2x</b><span>resolutions per support rep per day with Owner Agent</span></div>
  <div><b>224k+</b><span>past owner requests mined to decide what to build</span></div>
  <div><b>~2,000</b><span>discrete operations those requests reduce to</span></div>
  <div><b>100+</b><span>tools across 20+ domains</span></div>
</div>

<h2>What Owner Agent does</h2>
<p>
  OA needs a broad range of capabilities because Owner does many jobs for each restaurant (website,
  SEO, online ordering, branded app, loyalty, reporting, point of sale, and others).
</p>
<p>
  At one end of the range are simple actions. These are essential for the owners who won't open the
  dashboard to change their hours (or add a modifier, or change some text). They’ll call, even if it
  takes three clicks.
</p>
<p>With Owner Agent, they can get those things done with a conversation.</p>
<p>
  At the other end of the range, Owner Agent also needs to support sophisticated customers who want
  a deep understanding of their business and growth. Some owners run dozens of locations. They want
  to know what's moving on their menus, which locations are growing, how many corporate customers
  they serve, and whether regulars are coming back.
</p>
<figure>
  
<video class="astro-yngxbivw" src="https://building.owner.com/videos/advanced-questions.mp4" data-poster="https://building.owner.com/videos/advanced-questions.jpg" width="1600" height="900" muted loop playsinline preload="none" aria-label="Owner Agent answering a multi-location question" data-video="advanced-questions"></video>
  <figcaption>A multi-location question, answered from the restaurant's own data</figcaption>
</figure>
<p>
  Later, Owner Agent will start offering help without being asked, proactively, like suggesting a
  coupon in time for National Taco Day to drive app orders.
</p>
<p>
  We want Owner Agent to be the way owners reach everything we can do for them, not a sidebar bolted
  onto an app. For that reason, Owner Agent has entry points spread throughout the product (in
  addition to a prominent placement in the tab bar next to Home and Inbox):
</p>
<ul>
  <li>
    On the home screen, Owner offers to fix items in a "Needs Your Attention" section. Tapping it
    opens Owner Agent with a briefing on the problem, with the right location for the setting
    pre-attached.
  </li>
  <li>
    In the website builder, the agent appears beside the live preview. Edits show up in the preview
    as the owner approves them.
  </li>
  <li>
    Opening the agent from a menu item attaches that item to the conversation (e.g. "make this $2
    cheaper").
  </li>
  <li>
    Some onboarding steps, like connecting a domain, are passed to the agent if an owner gets stuck.
  </li>
</ul>
<p>
  There’s some cost to doing this; entry points have to be designed screen by screen, by the team
  that owns each screen, and each needs to give the agent enough context to be useful on the first
  message. We think it will pay off.
</p>

<h2>Where we started</h2>
<p>
  We picked our own customer-facing teams as our starting point. Those teams include more than 100
  people having thousands of conversations a week.
</p>
<p>
  Setting up a new restaurant's menu was one of their more time-consuming jobs. A rep would open a
  Google Drive with hundreds of food photos, half of them unlabeled, and try to match each one to a
  menu item by sight. Owner Agent can see images, so it reads the menu PDF and deconstructs it into
  items and modifiers. It matches each photo to a dish on the menu, and the rep can just approve it.
</p>
<figure>
  
<video class="astro-yngxbivw" src="https://building.owner.com/videos/menu-setup.mp4" data-poster="https://building.owner.com/videos/menu-setup.jpg" width="1600" height="900" muted loop playsinline preload="none" aria-label="Owner Agent sets up a menu from a PDF and photos" data-video="menu-setup"></video>
  <figcaption>
    A menu PDF and unlabeled photos become one approval card, photos matched to items
  </figcaption>
</figure>
<p>
  Starting with our team meant the agent got used heavily, every day, by people who knew the outcome
  they were hoping to get and could tell us when something didn’t work. Many of the tools owners use
  today were hardened there first. That has already shown up in the numbers: since reps started
  working in Support View with Owner Agent, resolutions per rep per day have more than doubled.
</p>
<p>
  Now we’re putting it in front of owners. The initial users have raved about it, including in the
  App Store, but there's still a lot to build.
</p>

<h2>Three building blocks: channel, tool, capability</h2>
<p>
  Every request to Owner Agent passes through three layers: from the channel where the request is
  received, through a tool, into a capability that’s proven by evals.
</p>
<dl class="primitives">
  <div>
    <dt><span>01</span>Channel</dt>
    <dd>
      How the owner reaches us: dashboard, app, email, or phone call. The channel decides how an
      action can be shown and approved, e.g. a complicated menu change is easy to review on a card
      and much harder to follow in a long email thread.
    </dd>
  </div>
  <div>
    <dt><span>02</span>Tool</dt>
    <dd>
      One narrow operation, like changing a menu price or updating a photo. Our codebase is full of
      functions that change hours or refund an order. A tool is the bridge between one of those
      functions and something the agent can call.
    </dd>
  </div>
  <div>
    <dt><span>03</span>Capability</dt>
    <dd>
      The set of tools and instructions that gets an owner to an outcome, like "update my menu for
      the fall." Each capability is proven by evals, which we'll describe in the section below on
      evals from historical support cases.
    </dd>
  </div>
</dl>

<h2>Keeping the owner in control</h2>
<p>Owners often send complex requests, like:</p>
<blockquote>
  Remove the pollo taco from the menu at the Miramar location. Also add tomatillo as a salsa option
  there. In Atlanta, raise the price of guac by $1.
</blockquote>
<p>
  A human support agent would always know to confirm the full list of changes in writing before
  taking action. Similarly, Owner Agent follows these rules to make changes safely:
</p>
<ol>
  <li>
    <strong>Get approval for every change.</strong> Owner Agent generates an approval card that
    describes the item, the location, and the change. It presents the “before” and “after” in plain
    language. Customers can approve and reject each row on the card individually. We call this bar
    "clarity of operation" internally.
  </li>
  <li>
    <strong>Act with the same permissions as the user.</strong> Tools use the same role-based access
    control as the rest of our platform. One reason this is important is that a restaurant employee
    might not have the same permissions as the owner. For example, if the employee can't issue
    refunds in the dashboard, then that person also won’t be able to use Owner Agent to issue one.
    This principle also holds when the agent works via email as opposed to an in-product chat
    window: it will still act on behalf of that user.
  </li>
  <li>
    <strong>Escalate to a human support rep when Owner Agent can’t solve the problem in one
    conversation.</strong> Owner Agent hands off to our support reps to pick up where it left off,
    including the full conversation history and context.
  </li>
</ol>

<h2>How we built it</h2>
<p>We have 4 principles that define our approach to building.</p>

<h3>Principle 1: One agent</h3>
<p>
  We’ve embedded the same agent, with the same tools, into all surfaces - including our dashboard
  and app, and also the tools our support team uses while communicating with customers.
</p>
<p>
  Emails and calls are powered by a background worker. On a call with a human support agent, the
  conversation is transcribed in real time. A small model opens an agent conversation for each of
  the customer’s requests, and it aims to make the change ready to approve by the time the owner
  finishes speaking.
</p>

<h3>Principle 2: Agent as a platform</h3>
<p>
  Almost everything an owner can do already exists as a service in our codebase. In theory we could
  have built up an Agents team to wrap all the parts of the product. But we’ve instead made Agent
  into a platform that all of our product teams are developing for. The Agents team owns the
  platform, including the agent loop, permissions, approvals, evals, and observability. But each pod
  owns its own tools.
</p>
<p>
  Every tool is built with <code>ownerTool</code>, a thin layer over the Vercel AI SDK's tool type.
  A tool declares typed inputs and outputs, whether it needs approval, which channels it can operate
  through, and the permission it requires.
</p>
<p>
  We picked AI SDK because it works across model providers, has incredible DX, and is one of the
  most popular agent frameworks out there. (When we started, OpenAI's Agents SDK had no TypeScript
  version.) Claude Agent SDK only supports Anthropic's models, which makes it non-viable almost
  immediately.
</p>

<h3>Principle 3: Read-first</h3>
<p>
  We built read tools first, and more than half of our tools still only read. For example: finding
  menu items, summing orders, ranking items, looking up customers, pulling reports.
</p>
<p>
  Read-only tools don't need approval. The agent can use as many of those as it needs before
  proposing an action.
</p>
<p>
  Having so many tools created a context problem, though. When we tried sending 100+ tools at once,
  it slowed the agent down significantly, reduced quality, and increased costs.
</p>
<p>We instead narrow down the list by permissions, surface, and channel.</p>
<p>
  Skills enable progressive loading of a domain's full instructions until they’re required for that
  task. We’ve also built a tool search function, which keeps tool definitions out of the context
  until they’re needed.
</p>

<h3>Principle 4: Evals from historical support cases</h3>
<p>
  With help from our BizOps team, we mined more than 224,000 past support cases from Snowflake and
  clustered them into scoreable capabilities. The cases have all been closed already. So for each
  case, we have full clarity on what the owner asked for (the input to an eval), what our team
  changed (the expected result), and how long it took (which is how we prioritize which capabilities
  to build first).
</p>
<figure>
  <img width="1600" height="991" decoding="async" src="https://building.owner.com/images/posts/eval-dash-screenshot-1.webp" alt="Eval dashboard showing support case categories" loading="lazy">
  <figcaption>The eval dashboard, top-level view</figcaption>
</figure>
<p>
  Menu updates account for roughly 10,000 of those cases - about 5%. We apply 3 levels of
  categorization to each case. That distilled the menu-updates cases to 414 distinct reasons. From
  there, we can prioritize the most time-consuming reasons, aiming for Owner Agent to cut into that
  time.
</p>
<figure>
  <img width="1600" height="1061" decoding="async" src="https://building.owner.com/images/posts/eval-dash-screenshot-2.webp" alt="Eval dashboard drilling into a case family ranked by time to close" loading="lazy">
  <figcaption>Case families ranked by support time, not volume</figcaption>
</figure>
<p>
  The cases overlap more than someone might think at first. If one case needs operations A, B, and
  C, and another needs A through D, then you just need four operations. We were able to distill our
  224,000 cases to about 2,000 distinct operations.
</p>
<figure>
  
<video class="astro-yngxbivw" src="https://building.owner.com/videos/cases-to-evals.mp4" data-poster="https://building.owner.com/videos/cases-to-evals.jpg" width="1600" height="900" muted loop playsinline preload="none" aria-label="From 224,000 support cases to evals" data-video="cases-to-evals"></video>
  <figcaption>224,000 cases collapse into about 2,000 operations, each backed by evals</figcaption>
</figure>
<p>
  There’s a difference between having all the necessary operations to handle a case, and handling
  the case. For example, some cases are so complicated that they can involve 70 interactions
  back-and-forth, with multiple reps involved. Our eval harness runs the real agent with its real
  tools against each case, a second model scores the result against the expected outcome, and the
  suites run in Buildkite.
</p>

<h2>Observing the agent in production</h2>
<p>
  Customers in production are asking Owner Agent for new things, beyond what the historical support
  data could have prepared it for. We've invested heavily in seeing what the agent does in the wild.
  Here are our main observability principles:
</p>
<p>
  <strong>Agent traces go to Datadog, the same place as the rest of our product APM.</strong> We run
  a trace from the request, through each model call and tool call, down to the database query. Each
  tool call also records a short note from the model on why it made the call. Finally, we record
  which model was used for each response, so that we can compare models eventually.
</p>
<p>
  <strong>Online evals score real conversations.</strong> We built Owner Agent Insights to read the
  conversations. Every few hours it samples finished conversations, always including any with a
  thumbs up or down. An LLM-as-judge model scores each conversation as good, needing improvement, or
  failed.
</p>
<p>
  <strong>Insights turns scores into a roadmap.</strong> Insights groups failures that share a cause
  and a solution, which turns scattered complaints into a tool recommendation. When owners ask for
  something the agent can't do, Insights writes it up as a Recommended Tool and alerts in Slack the
  team that owns that area.
</p>
<p>
  Requests that fit no area collect in what we call a ghost domain. A ghost domain that keeps growing
  is owners describing, in their own words, a part of Owner the agent doesn't cover yet.
</p>
<figure>
  
<video class="astro-yngxbivw" src="https://building.owner.com/videos/insights-loop.mp4" data-poster="https://building.owner.com/videos/insights-loop.jpg" width="1600" height="900" muted loop playsinline preload="none" aria-label="Owner Agent Insights: from graded conversations to a build brief" data-video="insights-loop"></video>
  <figcaption>From graded conversations to a Recommended Tool and a build brief</figcaption>
</figure>
<p>
  <strong>Every failure becomes an eval.</strong> When Insights flags a failure, Forge, our internal
  platform for background coding agents, turns the conversation into an offline eval case, works to
  solve it, and runs the evals in Buildkite. This ensures that the new case passes and that the rest
  that are solved will still pass too.
</p>

<h2>Lessons from building an agent</h2>
<p>
  If we were starting over, here are some of the principles we’d keep and implement from the
  beginning:
</p>
<ul>
  <li>
    Make the agent part of the product experience, and let the teams that own those surface areas
    design the entry points.
  </li>
  <li>
    Start from the requests customers have already made - support history is the closest thing you
    have to a spec.
  </li>
  <li>Match the agent’s permissions to the user’s.</li>
  <li>
    Build read tools first: any action is only as good as what the agent can read when taking that
    action.
  </li>
  <li>
    Make every change easy to confirm (one row per change, plain language, no action taken before
    human approval).
  </li>
  <li>Invest in observability early.</li>
  <li>Turn every failure into an eval.</li>
</ul>

<h2>What's next</h2>
<p>
  The biggest change ahead is proactive help. Owner Agent will notice things owners haven't asked
  about, like a decline in repeat orders or a holiday that could merit a coupon. Owner Agent will
  offer to act on these. We're also bringing it to more of the phone line.
</p>
<p>
  The owner in that car on Saturday should be able to say "close Lakeview at six tonight" and keep
  driving. Some days that already works. We’re making it work for everything Owner does, and the
  next post in this series will get deeper into how we’re building it.
</p>
<p>
  If this sounds like something you want to work on,
  <a href="https://www.owner.com/careers#roles">we're hiring</a>.
</p>]]></content:encoded>
      <enclosure url="https://building.owner.com/og/agent-1.png?v=3746ffa4" type="image/png" length="0" />
    </item>
    <item>
      <title>Zeus</title>
      <link>https://building.owner.com/zeus</link>
      <guid isPermaLink="true">https://building.owner.com/zeus</guid>
      <pubDate>Sun, 11 Oct 2026 12:00:00 GMT</pubDate>
      <dc:creator>Ryan Selden</dc:creator>
      <category>App</category>
      <description>Zeus is the internal platform behind 600+ Owner apps: one-click deploys, safe data access, built-in auth, and agent-driven migrations.</description>
      <content:encoded><![CDATA[<p class="astro-dhouf3sv">
  AI has shredded so many barriers to building: with the novelty of capable coding agents, we
  started to see a rapid expansion of internal apps and prototypes. We wanted anyone on the team to
  be able to build an app that could use internal data, have a mini-backend, connect to AI inference,
  and be shared securely.
</p>
<p class="astro-dhouf3sv">
  That was the vision for Zeus. We just crossed more than 600 internal apps &amp; prototypes running
  on the platform. We're near 1:1 apps to headcount, and we couldn't be more excited.
</p>
<figure class="astro-dhouf3sv">
  <img width="1600" height="900" decoding="async" src="https://building.owner.com/images/posts/zeus-apps.webp" alt="" loading="lazy" class="astro-dhouf3sv">
</figure>
<p class="astro-dhouf3sv">
  The PLG team built the PLG HQ, which drops confetti on demo day and gives everyone a bear to
  control (oh and it's also the central planning system for one of our busiest teams).
</p>
<figure class="astro-dhouf3sv">
  <img width="1600" height="916" decoding="async" src="https://building.owner.com/images/posts/zeus-plg-hq.webp" alt="" loading="lazy" class="astro-dhouf3sv">
</figure>
<p class="astro-dhouf3sv">
  Owner Wrapped gives engineers a fun way to see how their year at Owner is progressing (featuring
  our beloved customer, Lisa):
</p>
<figure class="wrapped-screens astro-dhouf3sv">
  <img width="978" height="1686" decoding="async" src="https://building.owner.com/images/posts/zeus-owner-wrapped-intro.webp" alt="Owner Wrapped welcome screen" loading="lazy" class="astro-dhouf3sv">
  <img width="946" height="1650" decoding="async" src="https://building.owner.com/images/posts/zeus-owner-wrapped-stats.webp" alt="Owner Wrapped showing an engineer's most-used commit words" loading="lazy" class="astro-dhouf3sv">
</figure>
<p class="astro-dhouf3sv">
  The POS health dashboard helps our team monitor devices and find issues that need attention:
</p>
<figure class="astro-dhouf3sv">
  <img width="1668" height="943" decoding="async" src="https://building.owner.com/images/posts/zeus-pos-health.webp" alt="POS health dashboard with device status summaries and a list of issues; sensitive details replaced with placeholders" loading="lazy" class="astro-dhouf3sv">
</figure>
<p class="astro-dhouf3sv">The design team made an app for brand assets:</p>
<figure class="astro-dhouf3sv">
  <img width="1600" height="916" decoding="async" src="https://building.owner.com/images/posts/zeus-brand-assets.webp" alt="" loading="lazy" class="astro-dhouf3sv">
</figure>
<p class="astro-dhouf3sv">We all needed the Owner Atlas app, so we could search for food near us:</p>
<figure class="astro-dhouf3sv">
  <img width="1600" height="927" decoding="async" src="https://building.owner.com/images/posts/zeus-owner-atlas.webp" alt="" loading="lazy" class="astro-dhouf3sv">
</figure>
<p class="astro-dhouf3sv">And one person really needed to go back to &#x27;95:</p>
<figure class="astro-dhouf3sv">
  <img width="1600" height="916" decoding="async" src="https://building.owner.com/images/posts/zeus-win95.webp" alt="" loading="lazy" class="astro-dhouf3sv">
</figure>
<h2 class="astro-dhouf3sv">How it works</h2>
<p class="astro-dhouf3sv">We built Zeus to solve 4 problems. </p>
<p class="astro-dhouf3sv">
  <strong class="astro-dhouf3sv">First problem:</strong> internal apps were being deployed in all kinds of ways. Before
  Zeus, new internal apps were appearing constantly with different hosts, databases, and vendors. We
  wanted one frontend system, with everything deployed in the same place with the same database
  setup. With Zeus you click “generate app,” and a minute later you have a web app (Tanstack Start
  or Astro, react, server-side rendering, file-based routing). Zeus makes a GitHub repo, provisions
  a Postgres database and a storage bucket, scaffolds the template into the repo, injects the
  secrets, and commits. GitHub Actions deploys it, and Zeus watches to make sure the first
  deployment completes.
</p>
<p class="astro-dhouf3sv">
  All apps run on Cloudflare&#x27;s Workers for Platforms. They share one namespace, and a single
  gateway worker sits in front and routes each request to the right app. This gives us room for
  10,000 apps before the next limit. Everything lives under one domain, and each app is a subdomain
  of owner.sh.
</p>
<p class="astro-dhouf3sv"><strong class="astro-dhouf3sv">2nd problem</strong>: giving every app safe access to our data.</p>
<p class="astro-dhouf3sv">
  Zeus gives every app built-in access to our systems and holds the credentials itself without a
  vendor key. It calls a Zeus proxy, and Zeus handles auth, rotation, and logging. Out of the box,
  an app can:
</p>
<ul class="astro-dhouf3sv">
  <li class="astro-dhouf3sv">
    Query Snowflake. One click mints a key, and the app queries through our Snowflake proxy. The app
    holds no Snowflake credentials of its own.
  </li>
  <li class="astro-dhouf3sv">
    Call an LLM. Every app routes AI calls through Poros, our internal inference router. We track
    cost per app, swap models, and fail over between vendors.
  </li>
  <li class="astro-dhouf3sv">
    Post to Slack. Flip on the integration and the app sends messages under its own name and avatar.
  </li>
  <li class="astro-dhouf3sv">
    Send email. A shared services worker sends email to owner.com addresses, so apps don’t each set
    up a mailer.
  </li>
  <li class="astro-dhouf3sv">Run scheduled jobs. A central scheduler triggers each app’s cron handler on time.</li>
</ul>
<p class="astro-dhouf3sv">
  <strong class="astro-dhouf3sv">3rd problem: </strong>auth, app permissions and security. Auth runs through Shibboleth,
  our sign-in portal. It uses our Okta SSO, lets in only owner.com accounts, and is completely
  separate from our production product auth. The gateway worker checks auth on every request before
  it reaches an app for extra security. If you&#x27;re not signed in, it sends you to Shibboleth
  first.
</p>
<p class="astro-dhouf3sv">
  Permissions are per app. An app is either open to anyone at the company or limited to people you
  grant in. Each grant carries a role, member or admin. You can bundle users into a group and grant
  the group across several apps. You manage all of it from the app’s page. Apps you share
  company-wide show up in the gallery.
</p>
<p class="astro-dhouf3sv">
  <strong class="astro-dhouf3sv">4th problem: </strong>we needed to be able to update all the apps agentically as we make
  updates in the future. Our migrations system creates an isolated session on a throwaway VM for
  each app.  The agent clones the app, makes the change, and opens a single PR. A manager session
  reviews all the PRs, retriggers updates when needed, and merges updates. We use this to bump
  dependencies, patch vulnerabilities, and roll out shared changes/upgrades. We can also run a
  read-only version that just reports back for compliance or tracking purposes.
</p>
<p class="astro-dhouf3sv">
  All app logs drain into Datadog so users can debug their apps and the company has visibility into
  what&#x27;s running. Zeus has a CLI for developers to manage and debug their apps.
</p>
<p class="astro-dhouf3sv">
  The result is that when you plug your AI coding tool into a Zeus app, it doesn’t need to ask you
  configuration questions that you may or may not be able to answer. Auth, database, deployment, CI,
  monitoring, environment. That’s all decided. You just describe what you want to build.
</p>
<h2 class="astro-dhouf3sv">100% adoption</h2>
<p class="astro-dhouf3sv">
  App growth has been steadily increasing since the early days of the Zeus launch. Our teams can
  just ship apps instead of spending time evaluating tools and onboarding to new services, with just
  a few exceptions. Adoption across our team has been pretty organic since it’s so quick and
  painless to get started on the platform.
</p>
<p class="astro-dhouf3sv">
  Every assumption we made about how small this would stay was wrong. And maybe this connects to
  where things are generally headed: the boundaries between engineering, design, and product are
  blurring. More people default to building. They see a problem and now have the tools to go do
  something about it. It used to require getting an engineer’s time, getting approval, getting API
  keys, getting access to systems. Zeus removes all of that. 
</p>
<h2 class="astro-dhouf3sv">A few things we learned<em class="astro-dhouf3sv"></em></h2>
<p class="astro-dhouf3sv"><strong class="astro-dhouf3sv">The first version was way too ambitious and overcomplicated. </strong></p>
<p class="astro-dhouf3sv">
  We tried to build our own system for deployments and CI, basically bypassing GitHub Actions. Users
  would push code directly into Zeus for processing. Presumably this would let us analyze code and
  add security guardrails. But that ended up being too restrictive for all the edge-cases of what
  people wanted to build. We scrapped it and went with the generator approach we have now, in which
  Zeus scaffolds a real repo with real CI and gets out of your way.
</p>
<p class="astro-dhouf3sv"><strong class="astro-dhouf3sv">At first, we didn&#x27;t plan for how fast it would grow. </strong></p>
<p class="astro-dhouf3sv">
  We were limited by how many workers we could deploy on one account. When we started, we barely
  thought about it, figuring it’d be a long time before we hit the limit. Then we passed 100 apps,
  and it became clear we’d blow through that limit quickly. That forced a migration to Workers for
  Platforms while also keeping all the existing standalone workers running.
</p>
<p class="astro-dhouf3sv"><strong class="astro-dhouf3sv">We needed a way to maintain hundreds, maybe thousands, of apps.</strong></p>
<p class="astro-dhouf3sv">
  The obvious risk with a platform like this is that apps get built, deliver some value, and then
  get forgotten. With 600+ apps and no guarantee each one has a long-term owner, that’s a problem.
  We built the migrations system where we can write a prompt for an AI agent and run it against
  every app on the platform at once. The apps don’t rot because the platform itself maintains them
  to a minimum standard.
</p>
<p class="astro-dhouf3sv"><strong class="astro-dhouf3sv">Expectations and needed features grew quickly</strong></p>
<p class="astro-dhouf3sv">
  Zeus began on a pure whim, and very much a side project to day-to-day responsibilities at Owner.
  Within a few short months, HR and finance built apps with highly sensitive data. A few apps were
  built that eclipsed the capacity of our original SQLite databases, and some of our builders
  clamored for dedicated compute features. Luckily, Owner has a supportive organization and we’ve
  managed to scale Zeus with contributions from VP Eng (thanks Max!) and now a second dedicated team
  member, Lucas.
</p>
<p class="astro-dhouf3sv">If you’re going to build this for your team: </p>
<p class="astro-dhouf3sv">
  You can’t predict what people will build. You can’t predict how complex the apps will be. You
  can’t predict the volume of apps. People will build more complex apps than you expect. They will
  build more apps than you expect. They’ll build single-purpose apps that will be used once in a
  meeting to make a decision, and then never again. They’ll build apps that become a key part of
  every demo and sales call we do across the company every day.
</p>
<p class="astro-dhouf3sv">
  Everything that used to be impossible can now be built, and this means people will also build
  buggy slop, or accidentally extend beyond key boundaries. For better or worse, it’s an arms race
  between the platform and the builders. Don’t underestimate someone’s crazy off-the-rails agent.
</p>
<p class="astro-dhouf3sv">
  As a starting point, pick the batteries that matter. For us, this was auth, hosting, and
  databases. That’s what people were repeatedly getting stuck on. If we’d tried to ship 30 different
  platform features on day 1, it probably would’ve been too complicated and nobody would have used
  it. Start with what people are already struggling with.
</p>
<p class="astro-dhouf3sv"><strong class="astro-dhouf3sv">Thanks for reading</strong></p>
<p class="astro-dhouf3sv">
  Building Zeus has been a blast, and a great learning experience. For better or worse, our industry
  has completely changed. As builders, we need to ensure we embrace AI tools and new ways of
  working, and I believe that we can use them as a force for good for our mission, customers, and
  team. I’m grateful to Owner for the trust to build aggressively, explore the frontier, and to
  continue scaling what we’ve found works. Hopefully this can inspire others to get out there and
  build.
</p>]]></content:encoded>
      <enclosure url="https://building.owner.com/og/zeus.png?v=0e9a8d33" type="image/png" length="0" />
    </item>
  </channel>
</rss>
