<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alex</title>
    <description>The latest articles on DEV Community by Alex (@thryvate).</description>
    <link>https://dev.to/thryvate</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4002546%2F3b5ed2d1-c03e-4e67-ac8f-dd0e569480b8.png</url>
      <title>DEV Community: Alex</title>
      <link>https://dev.to/thryvate</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thryvate"/>
    <language>en</language>
    <item>
      <title>Your AI wrote the HTML. Now decide what that page is allowed to talk to.</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Sun, 19 Jul 2026 15:49:46 +0000</pubDate>
      <link>https://dev.to/thryvate/your-ai-wrote-the-html-now-decide-what-that-page-is-allowed-to-talk-to-4nkg</link>
      <guid>https://dev.to/thryvate/your-ai-wrote-the-html-now-decide-what-that-page-is-allowed-to-talk-to-4nkg</guid>
      <description>&lt;p&gt;Locking down who can open a page is the part everyone thinks about. You put it behind auth, you invite people by email, you set an expiry. Done.&lt;/p&gt;

&lt;p&gt;Then the page loads and starts making requests you never looked at.&lt;/p&gt;

&lt;p&gt;This is the part I got wrong for a while, and I only really got pushed on it last week when someone on Indie Hackers said, roughly: private mode is only real if auth gates every asset before any bytes leave the server, and generated HTML can still leak through embedded data, public JSON, images, and third party scripts. They were right, and the second half of that sentence is the interesting half.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI-generated pages make this sharper
&lt;/h2&gt;

&lt;p&gt;When you write a page by hand, you know what is in it. When you ask a model for "a dashboard for this data" you get back something that works, and somewhere in it there may be a font from a CDN, an analytics snippet it added out of habit, a chart library pulled from unpkg, an image hotlinked from wherever the model saw one. None of that is malicious. It is just the model producing the most common shape of the thing you asked for.&lt;/p&gt;

&lt;p&gt;Now put that page behind a private link and hand it to a client. Every one of those outbound requests is a third party learning that someone opened your private page, and roughly when. An image URL alone can carry data in its query string.&lt;/p&gt;

&lt;p&gt;So "private" has two halves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inbound:&lt;/strong&gt; who can get the bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outbound:&lt;/strong&gt; what the bytes can reach once they run.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most sharing tools solve the first and quietly ignore the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  The inbound half, briefly
&lt;/h2&gt;

&lt;p&gt;For completeness, because the outbound half only matters if this part holds. Opening a site mints a short-lived signed token, and every sub-resource is served under that same token. There is no separate unprotected asset URL sitting on a CDN. On a gated site the token lives 120 seconds, so a copied asset URL is dead almost immediately. Expiry is checked before a token is ever minted, and on the data path too, so an expired link cuts off stored data the same way it cuts off content. Responses are &lt;code&gt;private, no-store&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is table stakes. Now the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The outbound half: default deny, then approve out loud
&lt;/h2&gt;

&lt;p&gt;The rule I landed on is that a published page can only load its own bundled assets unless the owner says otherwise. Concretely, the content renders in a sandboxed, cookieless, per-site origin under a CSP where &lt;code&gt;script-src&lt;/code&gt;, &lt;code&gt;style-src&lt;/code&gt;, &lt;code&gt;font-src&lt;/code&gt;, &lt;code&gt;connect-src&lt;/code&gt; and &lt;code&gt;frame-src&lt;/code&gt; are &lt;code&gt;'self'&lt;/code&gt; only, and each approved origin is woven into those directives explicitly. No wildcards. The host pattern is validated strictly before it ever reaches a CSP string, because a source expression is just text and text is injectable.&lt;/p&gt;

&lt;p&gt;Default deny is easy to write and horrible to ship, for one reason: it fails silently. The browser blocks the font, the page renders slightly wrong, and the owner has no idea why. So the deny has to come with a mouth. At publish time the files are scanned for external origins and the owner is told which ones were found and would be blocked. They approve the ones they meant, and the rest stay blocked. The approved set lives on the site as a list you can add to and remove from later, not a one-time dialog you clicked through.&lt;/p&gt;

&lt;p&gt;Two honest limits on that scan. It is regex over the common static shapes (a CDN script tag, a fonts link), so anything assembled at runtime slips past it, and it does not look at images at all, because &lt;code&gt;img-src&lt;/code&gt; is open anyway. It exists so nobody ships a half-rendered page. The CSP is the thing that enforces. The scan only tells you what the CSP is about to do, and it is best effort at that.&lt;/p&gt;

&lt;p&gt;The framing I got back from that same thread, which I liked better than my own: approved hosts should read as capabilities. "This page can contact these three origins." That is a sentence a non-technical owner can actually evaluate. A publish-time warning is a moment; a capability list is a property of the thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap I have not closed
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;img-src&lt;/code&gt; still allows any &lt;code&gt;https:&lt;/code&gt; origin.&lt;/p&gt;

&lt;p&gt;That was a convenience call, because pages hotlink images constantly and blocking them made the first-run experience feel broken. It is also, precisely, the side door: an image request is an outbound request, and its URL is a channel. Tightening it to the same approve-per-origin rule as everything else is the obvious fix and it is the top thing on my list. It means teaching the publish scan to look at images too, which it does not do today. I am writing this down here partly so it stays uncomfortable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure test worth stealing
&lt;/h2&gt;

&lt;p&gt;The best thing to come out of that whole exchange was a test, not a feature: &lt;strong&gt;can an expired page still trigger an outbound request from cached HTML?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It cuts through the marketing, because it does not ask what your policy says, it asks what a copy of your page does after you have revoked it. And the honest answer has a boundary in it. Anything we serve, we can cut off: page, assets, stored data, all gated before a token exists. But CSP is a response header. It travels with the response, not with the file. If a viewer saves the page to disk, that saved copy is out of reach of any expiry you set, and it will happily make whatever requests were baked into it.&lt;/p&gt;

&lt;p&gt;My first instinct was to write that this argues for tightening the outbound half &lt;em&gt;at publish time&lt;/em&gt; rather than at serve time, on the logic that what you never allowed into the document cannot be triggered later from a copy of it.&lt;/p&gt;

&lt;p&gt;That is wrong, and the reason is worth spelling out, because it is the trap. Tightening at publish, in a system like mine, still means adjusting a header that gets sent at serve time. The document itself is never rewritten. The &lt;code&gt;&amp;lt;img src="https://..."&amp;gt;&lt;/code&gt; stays exactly where the model put it, so a saved copy fires it regardless of what the policy said while the page was live.&lt;/p&gt;

&lt;p&gt;The only design that actually survives the test is rewriting the document at publish: inlining or proxying every external reference so there is no foreign URL left in the file to fire at all. That is a much bigger change than the &lt;code&gt;img-src&lt;/code&gt; fix, it breaks things people reasonably expect to work, and I have not built it.&lt;/p&gt;

&lt;p&gt;I am leaving the wrong version above rather than quietly deleting it, because it is the more natural thought, and if you are designing this you will probably have it too. It sounds like an architectural principle. It is really just a header.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you are building something similar
&lt;/h2&gt;

&lt;p&gt;Three things I would do again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make the deny default and the approval explicit, per origin, no wildcards.&lt;/li&gt;
&lt;li&gt;Scan at publish and tell the owner what you are about to block, in plain words. Silent security gets ripped out by the first user who thinks your product is broken. Then stay honest with yourself about what the scan misses, because a convenience feature starts feeling like a guarantee surprisingly fast.&lt;/li&gt;
&lt;li&gt;Write the gap down where users can see it. It is worse marketing and much better engineering.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I build &lt;a href="https://thryvate.com" rel="noopener noreferrer"&gt;Thryvate&lt;/a&gt;, which publishes what you make with AI as a private hosted site, so this is the problem I stare at. But nothing above is specific to it. If your AI is generating HTML that other people will open, you own both halves of the word private, whether or not you have looked at the second one.&lt;/p&gt;

&lt;p&gt;What is the outbound story where you work? I am curious whether anyone has actually shipped strict &lt;code&gt;img-src&lt;/code&gt; without the support burden eating them.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>ai</category>
      <category>csp</category>
    </item>
    <item>
      <title>Your AI built something private. A public link is the wrong way to share it.</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Wed, 15 Jul 2026 13:31:51 +0000</pubDate>
      <link>https://dev.to/thryvate/your-ai-built-something-private-a-public-link-is-the-wrong-way-to-share-it-138j</link>
      <guid>https://dev.to/thryvate/your-ai-built-something-private-a-public-link-is-the-wrong-way-to-share-it-138j</guid>
      <description>&lt;p&gt;Building with AI got fast. You describe a thing, and a page, a dashboard, a small&lt;br&gt;
internal report, or a client-facing microsite comes out. The awkward part is the&lt;br&gt;
next step: you want exactly three people to see it, and the default way we share a&lt;br&gt;
built thing is a public URL that anyone who gets the link can open.&lt;/p&gt;

&lt;p&gt;For a landing page that is fine. For a client's numbers, an unreleased feature, a&lt;br&gt;
draft you only want two colleagues to read, it is the wrong default. I kept hitting&lt;br&gt;
this while building &lt;a href="https://thryvate.com" rel="noopener noreferrer"&gt;Thryvate&lt;/a&gt; (disclosure: it is mine, this&lt;br&gt;
is a build-log, not a pitch), and the access model turned out to be the hardest and&lt;br&gt;
most interesting part. Here is where I landed and the tradeoffs behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with a link anyone can open
&lt;/h2&gt;

&lt;p&gt;Paste-to-link hosts (Tiiny.host, Static.app, Netlify Drop, and friends) are great at&lt;br&gt;
one job: take a file, give back a public URL, done. But "public URL" quietly decides&lt;br&gt;
three things for you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Anyone with the link is in. Forward it once and you have lost the guest list.&lt;/li&gt;
&lt;li&gt;You cannot tell who actually opened it, only that "someone" did.&lt;/li&gt;
&lt;li&gt;Search engines and link unfurlers can reach it, so "unlisted" is not "private."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a lot of AI-built artifacts, the whole point is that they are not for everyone.&lt;br&gt;
A URL that treats every viewer identically cannot express "these people, nobody&lt;br&gt;
else." So the model needs an identity for the viewer before it will show anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model I settled on: verify before a byte loads
&lt;/h2&gt;

&lt;p&gt;The rule I committed to is simple to say and annoying to build: no content renders&lt;br&gt;
until the viewer has proven who they are. Concretely, a private site does this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The viewer lands and gets an email-verification step, not the content.&lt;/li&gt;
&lt;li&gt;They enter their email, click the link, and only then does the page load.&lt;/li&gt;
&lt;li&gt;Their email is checked against an allowlist the owner controls. Not on it, not in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important word is &lt;em&gt;before&lt;/em&gt;. Verifying after render leaks the content to anyone&lt;br&gt;
who loads the page and closes the tab. Verifying before render means the bytes never&lt;br&gt;
leave the server for someone who is not on the list. That one ordering decision is&lt;br&gt;
most of the security value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Allowlist, not a forwardable link
&lt;/h2&gt;

&lt;p&gt;The allowlist is the piece that makes forwarding harmless. The site is bound to a&lt;br&gt;
set of email addresses, not to knowledge of a URL. If a viewer forwards the link,&lt;br&gt;
the recipient hits the same verification wall and bounces unless the owner added&lt;br&gt;
them. Adding and removing people is the owner editing a list, and revoking is&lt;br&gt;
immediate: drop the address and their next load fails.&lt;/p&gt;

&lt;p&gt;This is a different mental model from "unguessable link." An unguessable link is a&lt;br&gt;
password you accidentally paste into Slack. An allowlist is a guest list the door&lt;br&gt;
checks every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Password and expiry, layered on top
&lt;/h2&gt;

&lt;p&gt;Two smaller controls ride on top of the allowlist because real sharing is messier&lt;br&gt;
than a single switch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A per-site password, for when you want a lightweight gate without curating emails.&lt;/li&gt;
&lt;li&gt;A link expiry, for "this is fine to see this week, not next month."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They compose. You can require both an allowlisted email and a password, or set a&lt;br&gt;
private allowlisted site to also expire on Friday. The design goal was that the&lt;br&gt;
common cases (just me and two people; anyone at this company; public but only for&lt;br&gt;
a week) are all expressible without a settings PhD.&lt;/p&gt;

&lt;h2&gt;
  
  
  The origin decision: sandboxed and cookieless
&lt;/h2&gt;

&lt;p&gt;One more thing that is invisible until it bites you: an AI-built page can contain&lt;br&gt;
arbitrary HTML and JS. If every site renders on the same origin, one site's script&lt;br&gt;
can reach another's storage and cookies. So each site renders on its own sandboxed,&lt;br&gt;
cookieless origin. It costs some convenience (no shared login state across sites,&lt;br&gt;
by design) and buys real isolation between things different people published.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;Being honest about the ceiling, because "private by default" oversells easily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is not DRM. A verified viewer can screenshot or retype what they can see. The
model controls &lt;em&gt;who gets in&lt;/em&gt;, not what an authorized person does afterward.&lt;/li&gt;
&lt;li&gt;It is not anonymity for the viewer. The point is the opposite: the owner knows the
verified email of who is looking.&lt;/li&gt;
&lt;li&gt;It is not a replacement for real access control inside a complex app. This is for
sharing an artifact, not for building a multi-tenant product's permission system.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I think the default should flip
&lt;/h2&gt;

&lt;p&gt;The public-link hosts were designed for a web where you published things you wanted&lt;br&gt;
found. A lot of what we build with AI now is the opposite: specific, personal, meant&lt;br&gt;
for a named few. When the making is cheap, most of what gets made is not for the&lt;br&gt;
whole internet. So the sane default for sharing it is private, with the owner naming&lt;br&gt;
who gets in, and public as the deliberate exception.&lt;/p&gt;

&lt;p&gt;If you have built something with AI and the "now share it with just these people"&lt;br&gt;
step felt wrong, I would genuinely like to hear how you handled it. And if you want&lt;br&gt;
to see the access model above in practice, &lt;a href="https://thryvate.com" rel="noopener noreferrer"&gt;Thryvate&lt;/a&gt; is where&lt;br&gt;
I built it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>security</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Your AI built a static page. It can't store anything. Here's the fix I settled on.</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Fri, 10 Jul 2026 14:26:23 +0000</pubDate>
      <link>https://dev.to/thryvate/your-ai-built-a-static-page-it-cant-store-anything-heres-the-fix-i-settled-on-3jlf</link>
      <guid>https://dev.to/thryvate/your-ai-built-a-static-page-it-cant-store-anything-heres-the-fix-i-settled-on-3jlf</guid>
      <description>&lt;p&gt;An AI can build you a full page in one prompt now. A leaderboard, an RSVP form, a little internal dashboard, a guestbook. It looks done. Then someone types their name into the form, hits submit, and nothing happens, because there is no server on the other end. It's just HTML sitting in a bucket.&lt;/p&gt;

&lt;p&gt;I kept hitting this. The generated page was 90% there and the last 10% was "wire up a database," which quietly means: stand up a backend, get API keys, handle auth, deploy it, keep it running. For a page you built in four minutes to share with a few people, that is a wildly lopsided amount of work. So most of these pages just stay read-only forever, or die in a folder.&lt;/p&gt;

&lt;p&gt;I build a hosting product for AI-generated sites (Thryvate, it's mine, disclosing up front), and this was the most common dead end people hit. So I spent a while on the storage half specifically. This post is about what I landed on and the tradeoffs, not a pitch. If you're wiring your own AI-built pages to Supabase or Firebase, some of this might save you a step.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint that shapes everything
&lt;/h2&gt;

&lt;p&gt;The page is static. The person who "wrote" it was an AI, prompted by someone who may not code. There is no build step they control, no env file, no place to safely put a secret. So the usual answer, "just add Supabase," breaks on step one: a public static page can't hold a service key, and asking a non-dev to set up row-level security is a non-starter.&lt;/p&gt;

&lt;p&gt;That pushes you to one design: the data layer has to be ambient. Present on every page with zero setup, no keys in the HTML, and the trust boundary enforced by the host, not by code the page ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I ended up with
&lt;/h2&gt;

&lt;p&gt;A single global the page can assume is there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// save a record; the collection is created on first write&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;THRYVATE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rsvp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;coming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// read it back&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;THRYVATE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rsvp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coming&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;fetch&lt;/code&gt;, no endpoint, no key. Each record comes back as &lt;code&gt;{ id, value, created, actor }&lt;/code&gt; and you read your own fields off &lt;code&gt;record.value&lt;/code&gt;. The AI generating the page only has to know this one object exists, which turns out to be easy to teach it, because it's a tiny surface.&lt;/p&gt;

&lt;p&gt;A few decisions inside that were less obvious than they looked:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collections are created on first write.&lt;/strong&gt; No migration, no schema step. The person prompting the AI never says "create a table." The first &lt;code&gt;append&lt;/code&gt; to a new name makes it. This matters more than it sounds: every explicit setup step is a place a non-dev abandons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity is resolved by the host, and the page can't lie about it.&lt;/strong&gt; There's a &lt;code&gt;viewer()&lt;/code&gt; call that returns who is looking at the page, but only when the site is actually behind auth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;THRYVATE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;viewer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// private site, signed-in viewer -&amp;gt; { id: 'sam@work.com', kind: 'viewer' }&lt;/span&gt;
&lt;span class="c1"&gt;// public or password-only          -&amp;gt; { kind: 'anonymous' }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule I enforce: if you want to show WHO did something (a high score, a clock-in, a comment), you call &lt;code&gt;viewer()&lt;/code&gt; and write that identity into the record yourself. You cannot read back the &lt;code&gt;actor&lt;/code&gt; on someone else's record to display it, that field is redacted in-page down to just a &lt;code&gt;kind&lt;/code&gt;. Otherwise any guestbook becomes an email scraper. It took a couple of rewrites to get that boundary in the right place: useful enough to build a real leaderboard, locked down enough that a public page can't harvest who visited.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Current state" vs "full history" is one flag.&lt;/strong&gt; Append-only logs are honest but annoying when you actually want the latest value per key (a settings row, the current status). So &lt;code&gt;query&lt;/code&gt; takes &lt;code&gt;latest: true&lt;/code&gt; and folds to the newest record per &lt;code&gt;key&lt;/code&gt; field. You get an event log for free and a state view when you want it, without two different storage models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is the wrong tool
&lt;/h2&gt;

&lt;p&gt;Being honest about the ceiling, because dev.to will (correctly) push back otherwise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's records-in, records-out with equality filters and ordering. No joins, no server-side aggregation, no transactions. If you're building something with real relational needs, you want an actual backend and the setup cost that comes with it.&lt;/li&gt;
&lt;li&gt;The whole point is the page can't hold a secret, so anything requiring server-side trust (charging a card, calling a paid API with your key) is out of scope by design. That belongs on a server.&lt;/li&gt;
&lt;li&gt;It's tied to the page being hosted somewhere that provides the runtime. A raw file on S3 won't grow a &lt;code&gt;window.THRYVATE&lt;/code&gt; on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway that isn't about my product
&lt;/h2&gt;

&lt;p&gt;If you're the one gluing AI-generated pages to a database, the thing worth stealing is the constraint, not the implementation: assume the page can hold no secrets and the person shipping it won't configure anything. Design the data layer so identity and access are enforced by the host and the page just calls a verb. Once you accept that, a lot of the "which BaaS" debate collapses, because most of them assume a setup step your actual user will never do.&lt;/p&gt;

&lt;p&gt;Curious how others are handling the storage half for vibe-coded pages. Are you defaulting people to Supabase and eating the setup, or avoiding stateful pages entirely?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I gave my AI a "publish this" tool over MCP. Here is what that tool actually has to do.</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Mon, 06 Jul 2026 10:03:36 +0000</pubDate>
      <link>https://dev.to/thryvate/i-gave-my-ai-a-publish-this-tool-over-mcp-here-is-what-that-tool-actually-has-to-do-3h19</link>
      <guid>https://dev.to/thryvate/i-gave-my-ai-a-publish-this-tool-over-mcp-here-is-what-that-tool-actually-has-to-do-3h19</guid>
      <description>&lt;p&gt;The making got 10x easier. The last mile did not.&lt;/p&gt;

&lt;p&gt;You can now ask an AI to build a working thing: a dashboard, a small app, a one-pager, a report. It hands you code or a file, and then you hit the dumb wall. The artifact exists, but getting it in front of a specific person means exporting, finding hosting, wiring auth, or just sending a zip that kills the moment.&lt;/p&gt;

&lt;p&gt;So I started building the missing verb. Not "generate," which is solved, but "publish this." One sentence to the AI, and the thing it just built becomes a real, hosted, live site behind access control. I build &lt;a href="https://thryvate.com" rel="noopener noreferrer"&gt;Thryvate&lt;/a&gt;, so this is the design I landed on, but the interesting part is not the product, it is what a &lt;code&gt;publish&lt;/code&gt; tool actually has to guarantee once an autonomous agent is the one calling it. Full disclosure up front so you can weigh the rest.&lt;/p&gt;

&lt;p&gt;Here is what I learned the tool has to do, past the obvious "accept some HTML."&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Update in place, or you get link rot on every iteration
&lt;/h2&gt;

&lt;p&gt;The naive tool signature is &lt;code&gt;publish(html) -&amp;gt; url&lt;/code&gt;. That is wrong the moment the agent iterates. The user says "make the header smaller," the agent regenerates, and if &lt;code&gt;publish&lt;/code&gt; mints a fresh URL every time, the link you already shared to a client is now stale.&lt;/p&gt;

&lt;p&gt;So the real contract is closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;publish(html) -&amp;gt; { site_id, url }
update(site_id, html) -&amp;gt; { url }   // same url, new version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent has to carry &lt;code&gt;site_id&lt;/code&gt; across turns and default to &lt;code&gt;update&lt;/code&gt; once a site exists. "Publish this" on turn one and "tweak it" on turn five must land on the same URL. That single decision (stable identity separate from content) removes the most common footgun of agent-driven publishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Every update is a version, and rollback is one call
&lt;/h2&gt;

&lt;p&gt;Once an agent can overwrite your live site unattended, you need an undo that does not depend on the agent having kept the old output. If turn five produced broken HTML, "ask the AI to regenerate the good version" is not a recovery plan, it is a prayer.&lt;/p&gt;

&lt;p&gt;So every &lt;code&gt;update&lt;/code&gt; is an append, not a replace, and there is a first-class &lt;code&gt;rollback(site_id, version)&lt;/code&gt;. The agent is allowed to move fast because a human (or the agent itself) can step back instantly. Treat published versions like git history, not like a single mutable file.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The access model is the actual product, not a checkbox
&lt;/h2&gt;

&lt;p&gt;This is the part I underestimated. Paste-to-link hosts (Tiiny, Static, Netlify Drop, and friends, all genuinely good at what they do) give you a public URL: anyone with the link can open it. That is exactly right for a landing page and exactly wrong for the thing your AI built for one client.&lt;/p&gt;

&lt;p&gt;When an agent publishes on your behalf, the safe default has to be private, because the agent cannot judge sensitivity. So the tool's access surface is where most of the design went:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set_visibility(site_id, "private" | "public")
set_allowlist(site_id, ["a@co.com", "b@co.com"])   // invite by identity
set_view_password(site_id, "...")                   // optional second factor
set_link_expiry(site_id, "2026-08-01T00:00:00Z")    // auto-dark after
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wedge is verified access, not a secret URL. A private site should make every viewer verify their email before a single byte of the page loads, so "who can see it" is an allowlist you control, not a link that gets forwarded into a group chat. A shared link that anyone can forward is not access control, it is hope.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Render it in a sandbox, because the AI wrote it
&lt;/h2&gt;

&lt;p&gt;You are hosting arbitrary AI-generated HTML and JS on your domain. Serve it from the same origin as your app and one generated &lt;code&gt;fetch&lt;/code&gt; or cookie read can reach places it should not.&lt;/p&gt;

&lt;p&gt;The fix is boring and non-negotiable: sandboxed, cookieless origin per site. Each published page runs isolated from your session and from every other site. When the author is a language model, "assume the payload is hostile" is just the correct default, even when the author had good intentions.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Give the published page a data backend, or it is only ever a brochure
&lt;/h2&gt;

&lt;p&gt;The thing that surprised me most: as soon as the AI can publish a live page, people immediately want that page to &lt;em&gt;do&lt;/em&gt; something. Collect an RSVP. Record a high score. Take a form submission. A static file cannot, and asking a non-technical user to "stand up a backend" puts the wall right back.&lt;/p&gt;

&lt;p&gt;So the published page ships with a tiny global SDK, no keys, no setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// inside the AI-generated page&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;THRYVATE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rsvps&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;THRYVATE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rsvps&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the AI can build a working guestbook, a poll, a signup, or a leaderboard, and it just persists. The publish tool is not shipping a document, it is shipping a small application with somewhere to put its data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool surface, in one place
&lt;/h2&gt;

&lt;p&gt;Pulling it together, a "publish this" tool an agent can safely drive ends up being roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;publish&lt;/code&gt; / &lt;code&gt;update&lt;/code&gt; with stable &lt;code&gt;site_id&lt;/code&gt; (no link rot)&lt;/li&gt;
&lt;li&gt;versioning + &lt;code&gt;rollback&lt;/code&gt; (fearless iteration)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;set_visibility&lt;/code&gt; / &lt;code&gt;set_allowlist&lt;/code&gt; / &lt;code&gt;set_view_password&lt;/code&gt; / &lt;code&gt;set_link_expiry&lt;/code&gt; (private by default, verified access)&lt;/li&gt;
&lt;li&gt;sandboxed per-site origin (untrusted author)&lt;/li&gt;
&lt;li&gt;an in-page data SDK (published pages that actually do things)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are exotic on their own. What is new is that the &lt;em&gt;caller&lt;/em&gt; is an autonomous agent, so the defaults have to be safe without a human in the loop for each step: private not public, versioned not overwritten, sandboxed not trusted.&lt;/p&gt;

&lt;p&gt;If you are wiring your own agent to ship things to the web, I think those five are the load-bearing pieces, whatever you build it on. And if you would rather not build the plumbing, that is the whole reason &lt;a href="https://thryvate.com" rel="noopener noreferrer"&gt;Thryvate&lt;/a&gt; exists: your AI publishes what it made straight to a hosted, private-by-default site, and you control who gets in. Happy to answer anything about the access model in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>devtools</category>
      <category>mcp</category>
    </item>
    <item>
      <title>What changes when an AI agent can publish to the public web</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Sat, 27 Jun 2026 18:14:32 +0000</pubDate>
      <link>https://dev.to/thryvate/what-changes-when-an-ai-agent-can-publish-to-the-public-web-1eh9</link>
      <guid>https://dev.to/thryvate/what-changes-when-an-ai-agent-can-publish-to-the-public-web-1eh9</guid>
      <description>&lt;p&gt;I've been building agent workflows for a while, and one capability keeps coming up that the ecosystem hasn't fully reckoned with: letting an AI agent &lt;strong&gt;publish a document to the public internet&lt;/strong&gt; and hand someone a link.&lt;/p&gt;

&lt;p&gt;It sounds trivial ("save HTML, return a URL"). It isn't. The moment an autonomous agent can mint a public link, you've handed it a primitive that touches access control, data exposure, and reputation. This post is about the design questions that surface once you take that seriously, written by someone who builds in this space. Disclosure up front: I work on Thryvate, a document-sharing tool with an MCP server. More on that at the end, but the problems below are general.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive version
&lt;/h2&gt;

&lt;p&gt;The first version everyone writes is a tool that takes content and dumps it to object storage behind a public CDN URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;publish(html) -&amp;gt; https://cdn.example.com/a8f3c2.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ship that and an agent can now share its work. It can also now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;expose a half-finished draft to anyone who guesses the URL,&lt;/li&gt;
&lt;li&gt;leave that URL live forever with no way to pull it back,&lt;/li&gt;
&lt;li&gt;publish something containing a customer's name with zero record of who saw it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a human hitting "publish" deliberately, those are acceptable defaults. For an agent doing it as one step in a longer plan, they're landmines.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "publish" should actually mean for an agent
&lt;/h2&gt;

&lt;p&gt;A few properties turn the naive primitive into something you'd trust an agent to call:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Default to private, opt into public.&lt;/strong&gt; The safe default for an agent-minted link is &lt;em&gt;not&lt;/em&gt; "world-readable." It's "only people on this list" or "only people with the password." Public should be an explicit parameter someone has to set, not the fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Revocability.&lt;/strong&gt; Anything an agent publishes, you must be able to un-publish instantly. A live link is a liability with a half-life, and the ability to revoke is what makes it safe to let the agent create them liberally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Expiry as a first-class field.&lt;/strong&gt; "This link dies in 7 days" should be a parameter on the publish call, not a cron job you remember to write later. Most agent-published artifacts are ephemeral by nature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Per-viewer visibility.&lt;/strong&gt; If the agent is sharing a deck or report, the human almost always wants to know &lt;em&gt;who opened it and when&lt;/em&gt;. That telemetry is also how you detect a leak, since an unfamiliar viewer is a signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Idempotent updates.&lt;/strong&gt; Agents iterate. The second draft should update the same URL, not spray new ones. The link is the stable handle, and the content behind it changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where MCP fits
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; is a clean place to expose exactly this surface. Instead of an agent shelling out to &lt;code&gt;aws s3 cp&lt;/code&gt;, you give it a small set of typed tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;publish_site&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visibility&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;set_link_expiry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;add_to_allowlist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;get_analytics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the access-control model lives in the tool layer, where it can be enforced and audited, instead of being improvised by the model inside a bash command. The agent reasons about &lt;em&gt;intent&lt;/em&gt; ("share this with the investor"), and the tools enforce &lt;em&gt;policy&lt;/em&gt; (private by default, expiring, owner-scoped). That separation is the whole game: the model is creative, the tools are strict.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I keep getting wrong
&lt;/h2&gt;

&lt;p&gt;The temptation is to let the agent set everything, including visibility. Resist it. The single most useful guardrail I've landed on: &lt;strong&gt;the agent can draft and stage a link, but flipping something to fully public stays a deliberate, reviewable step.&lt;/strong&gt; It keeps the worst failure mode at "a draft sat private a little too long" instead of "an agent published the wrong thing to the open web."&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Disclosure: I build &lt;a href="https://thryvate.com" rel="noopener noreferrer"&gt;Thryvate&lt;/a&gt;, a document-sharing app that ships an MCP server doing roughly the above, with private-by-default links, allowlists, expiry, and per-viewer analytics. None of the design points here are specific to it, though. If you're wiring publish-to-web into your own agent, these are the questions worth answering first.&lt;/p&gt;

&lt;p&gt;I'm genuinely curious how others are handling &lt;strong&gt;revocation&lt;/strong&gt; and &lt;strong&gt;default visibility&lt;/strong&gt; for agent-published artifacts. Drop a note if you've shipped this.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
