<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Posts tagged #outseta</title><description>All posts tagged with outseta on queen.raae.codes</description><link>https://queen.raae.codes/tag/outseta/</link><item><title>📝 ✨ ~ Customer Bastien: your MCP server has a permission problem</title><link>https://queen.raae.codes/2026-03-10-mcp-tool-annotations/</link><guid isPermaLink="true">https://queen.raae.codes/2026-03-10-mcp-tool-annotations/</guid><description>Back in August I built the Outseta MCP server MVP to showcase how one could enable Jean-Claude (my Claude Code instance) and other AI tools to manage stuff in…</description><pubDate>Tue, 10 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Back in August I built the &lt;a href=&quot;https://github.com/outseta/outseta-admin-mcp-server&quot;&gt;Outseta MCP server MVP&lt;/a&gt; to showcase how one could enable Jean-Claude (my Claude Code instance) and other AI tools to manage stuff in your &lt;a href=&quot;https://outseta.com?via=queen&quot;&gt;Outseta&lt;/a&gt; account. Stuff like users, billing, email lists etc.&lt;/p&gt;
&lt;p&gt;No delete tools were included, but it had both write and update tools. After months of very little feedback &lt;a href=&quot;https://github.com/outseta/outseta-admin-mcp-server/issues/2&quot;&gt;Bastien opened an issue&lt;/a&gt; that made me go &amp;quot;oh, I didn&apos;t even know that was possible.&amp;quot;&lt;/p&gt;
&lt;h2&gt;The problem I hadn&apos;t noticed&lt;/h2&gt;
&lt;p&gt;When you add an MCP server to Claude Desktop, it asks what permission level you want for the tools. Always allow, require approval, or never. Pretty standard.&lt;/p&gt;
&lt;p&gt;The catch? Claude Desktop was showing all 15 Outseta tools as one undifferentiated blob. &amp;quot;Other tools.&amp;quot; So your choices were: decide permission settings for all. Or do them one by one.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./outseta-tools-before.png&quot; alt=&quot;Outseta tools shown as one group in Claude Desktop&quot;&gt;&lt;/p&gt;
&lt;p&gt;Bastien pointed to the &lt;a href=&quot;https://github.com/makenotion/notion-mcp-server&quot;&gt;Notion MCP server&lt;/a&gt; as the reference. There, read-only tools and write tools show up as separate groups. You can batch set the permission for each group 🤯&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./notion-tools-grouped.png&quot; alt=&quot;Notion MCP server with tools grouped by read and write&quot;&gt;&lt;/p&gt;
&lt;p&gt;I had not noticed that MCP servers could do that.&lt;/p&gt;
&lt;h2&gt;The fix: tool annotations&lt;/h2&gt;
&lt;p&gt;Turns out the MCP spec has an annotations system. You set hints on each tool — &lt;code&gt;readOnlyHint&lt;/code&gt;, &lt;code&gt;destructiveHint&lt;/code&gt;, &lt;code&gt;openWorldHint&lt;/code&gt; — and the client uses those to group and scope permissions.&lt;/p&gt;
&lt;p&gt;I defined three tiers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const READ_ANNOTATION = {
  readOnlyHint: true,
  destructiveHint: false,
  openWorldHint: true,
} as const;

const WRITE_ANNOTATION = {
  readOnlyHint: false,
  destructiveHint: false,
  openWorldHint: true,
} as const;

const DESTRUCTIVE_ANNOTATION = {
  ...WRITE_ANNOTATION,
  destructiveHint: true,
} as const;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then categorized all 15 tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;7 read-only&lt;/strong&gt; — &lt;code&gt;get_accounts&lt;/code&gt;, &lt;code&gt;get_people&lt;/code&gt;, &lt;code&gt;get_plans&lt;/code&gt;, &lt;code&gt;get_plan_families&lt;/code&gt;, &lt;code&gt;get_email_lists&lt;/code&gt;, &lt;code&gt;get_email_list_subscribers&lt;/code&gt;, &lt;code&gt;preview_subscription_change&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;7 write&lt;/strong&gt; — &lt;code&gt;register_account&lt;/code&gt;, &lt;code&gt;create_person&lt;/code&gt;, &lt;code&gt;add_person_to_account&lt;/code&gt;, &lt;code&gt;create_plan&lt;/code&gt;, &lt;code&gt;create_plan_family&lt;/code&gt;, &lt;code&gt;create_email_list&lt;/code&gt;, &lt;code&gt;subscribe_to_email_list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1 destructive&lt;/strong&gt; — &lt;code&gt;change_subscription&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And now Claude Desktop shows them as separate groups with independent permission settings:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./outseta-tools-after.png&quot; alt=&quot;Outseta tools grouped by read-only and write/delete in Claude Desktop&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Why &lt;code&gt;change_subscription&lt;/code&gt; gets the destructive flag&lt;/h2&gt;
&lt;p&gt;This was the one I had to think about. Most write tools here are creating things — a new person, a new plan. Easy to undo. But changing a subscription hits billing. Prorations, invoice changes, real money moving. That&apos;s not a casual &amp;quot;oops, delete it&amp;quot; situation.&lt;/p&gt;
&lt;p&gt;Claude Desktop doesn&apos;t actually render a separate group for destructive vs. regular writes — yet. But &lt;code&gt;destructiveHint&lt;/code&gt; is in the spec for a reason. When clients start using it, the annotation is already there. And honestly, it&apos;s just good documentation. Anyone reading the tool list can see: this one has consequences.&lt;/p&gt;
&lt;h2&gt;The API change&lt;/h2&gt;
&lt;p&gt;The SDK has two ways to register tools. The positional-args version (&lt;code&gt;server.tool()&lt;/code&gt;) doesn&apos;t support annotations cleanly. The config-object version (&lt;code&gt;server.registerTool()&lt;/code&gt;) does:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;server.registerTool(
  &amp;quot;get_accounts&amp;quot;,
  {
    description: &amp;quot;Query accounts with filtering and pagination&amp;quot;,
    inputSchema: GetAccountsSchema.shape,
    annotations: READ_ANNOTATION,
  },
  async (params) =&amp;gt; {
    // ...
  },
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both are built into &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt;. No custom code needed. I just hadn&apos;t used &lt;code&gt;registerTool&lt;/code&gt; before.&lt;/p&gt;
&lt;h2&gt;If you&apos;re building an MCP server&lt;/h2&gt;
&lt;p&gt;Three things I&apos;d steal from this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Look at the Notion MCP server.&lt;/strong&gt; I keep hearing good things about it, and it clearly uses the spec features well. A solid reference if you&apos;re figuring out annotations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Annotate your tools from day one.&lt;/strong&gt; The three-constant pattern (&lt;code&gt;READ&lt;/code&gt;, &lt;code&gt;WRITE&lt;/code&gt;, &lt;code&gt;DESTRUCTIVE&lt;/code&gt;) covers most cases. Your users get granular permissions for free.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Think about what &amp;quot;destructive&amp;quot; means in your domain.&lt;/strong&gt; For Outseta, it&apos;s billing mutations — and deletions too, when we add those. For your tool it might be deleting records, sending emails, or modifying permissions. If the user would want a confirmation dialog, it&apos;s probably destructive.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;Customer like Bastien are worth their weight in gold 🙏&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./bastien-thanks.png&quot; alt=&quot;Bastien confirming it works&quot;&gt;&lt;/p&gt;
&lt;p&gt;Building an MCP server? I&apos;m curious what what other patterns you&apos;ve discovered that I should know about. &lt;a href=&quot;mailto:queen@raae.codes&quot;&gt;Hit me up&lt;/a&gt;!&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ The insight layer your SaaS is missing</title><link>https://queen.raae.codes/2026-03-09-build-the-insight-layer/</link><guid isPermaLink="true">https://queen.raae.codes/2026-03-09-build-the-insight-layer/</guid><description>An agent wants to know which onboarding emails aren&apos;t landing. Right now it downloads everything, reads through it all, figures out the patterns. Every time.…</description><pubDate>Mon, 09 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;An agent wants to know which onboarding emails aren&apos;t landing. Right now it downloads everything, reads through it all, figures out the patterns. Every time. For every user, every session. That&apos;s expensive, slow, and wasteful.&lt;/p&gt;
&lt;p&gt;What if the SaaS provider did that work once?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;If we as service providers can provide a layer on top of our content with some vector search and some thematic extraction — we run a little AI on our side that could pull out themes.&amp;quot;
&amp;lt;cite&amp;gt;🎧 Me on &lt;a href=&quot;https://slowandsteadypodcast.com/236?#t=36:16&quot;&gt;Slow &amp;amp; Steady 236@36:16 (February 2026)&lt;/a&gt; ↓&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&amp;quot;100%&amp;quot; height=&amp;quot;180&amp;quot; frameborder=&amp;quot;no&amp;quot; scrolling=&amp;quot;no&amp;quot; seamless=&amp;quot;&amp;quot; src=&amp;quot;https://share.transistor.fm/e/0ec939c2?#t=36:16&amp;quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;Pre-process the data. Extract themes, compute scores, build embeddings. The agent asks for themes first, then drills into the specific content it needs. Two steps instead of downloading the whole archive every time.&lt;/p&gt;
&lt;h2&gt;I did this with podcast transcripts&lt;/h2&gt;
&lt;p&gt;I&apos;ve built exactly this for the Slow &amp;amp; Steady podcast. Raw transcripts go in, and out comes a structured knowledge base: ideas extracted, stories tagged, quotable moments indexed by theme. When I ask Jean-Claude (my Claude Code instance) &amp;quot;what should I blog about?&amp;quot;, it doesn&apos;t read through 236 episodes of raw audio transcripts. It searches the processed knowledge base, finds the themes, then pulls the specific quotes it needs to give me ideas.&lt;/p&gt;
&lt;p&gt;(Sidenote: if you want this for your podcast, &lt;a href=&quot;mailto:queen@raae.codes?subject=Podcast%20pipeline&quot;&gt;drop me a line&lt;/a&gt;.)&lt;/p&gt;
&lt;h2&gt;Now imagine this for your SaaS&lt;/h2&gt;
&lt;p&gt;So I pitched Benedikt, my-cohost and the founder of &lt;a href=&quot;https://userlist.com/?via=queen&quot;&gt;Userlist&lt;/a&gt;, on doing something similar for their users&apos; emails. Their MCP server can do CRUD: list users, get a broadcast, create a campaign. But what if it could also answer &amp;quot;which onboarding emails aren&apos;t landing?&amp;quot; or &amp;quot;what should my next broadcast be about?&amp;quot; without the agent doing all the analysis itself? Pre-process the engagement data, and the agent gets the answer in one call.&lt;/p&gt;
&lt;p&gt;At &lt;a href=&quot;https://outseta.com?via=queen&quot;&gt;Outseta&lt;/a&gt; we&apos;re in the same spot. Our MCP MVP mirrors the API. Fine for basic operations. But the questions we actually want agents to answer aren&apos;t CRUD:&lt;/p&gt;
&lt;p&gt;&amp;quot;Which customers are at risk?&amp;quot; — that needs a computed score, not a list endpoint.
&amp;quot;What topics drive conversions?&amp;quot; — that needs pattern analysis across email and billing data.
&amp;quot;Where are users getting stuck?&amp;quot; — that needs theme extraction from support tickets.&lt;/p&gt;
&lt;p&gt;If we pre-process this, build the insights on our side so the agent gets patterns instead of spending tokens discovering them every time, I think we&apos;ll be even more valuable to our customers. And their agents.&lt;/p&gt;
&lt;h2&gt;Agents and humans, same insights&lt;/h2&gt;
&lt;p&gt;But while we are at it, let&apos;s not limit insights to the agent layer. Build the insight layer into your product and expose it through the UI, API, MCP, CLI, whatever comes next.&lt;/p&gt;
&lt;p&gt;The interface changes. The insights stay.&lt;/p&gt;
&lt;p&gt;At Outseta we have billing, email, support, CRM all in one place. A &lt;a href=&quot;/2026/03/07-outseta-a-system-of-record/&quot;&gt;system of record&lt;/a&gt; so to speak. Now the question is: what insights do we build on top of it?&lt;/p&gt;
&lt;p&gt;The smartest API is the one that already did the thinking.&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;li&gt;Userlist&apos;s co-founder Benedikt Deicke is Queen Raae&apos;s co-host on Slow &amp; Steady podcast.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ Outseta, a system of record?</title><link>https://queen.raae.codes/2026-03-07-outseta-a-system-of-record/</link><guid isPermaLink="true">https://queen.raae.codes/2026-03-07-outseta-a-system-of-record/</guid><description>At Outseta we have billing, auth, email, support, and CRM under one roof. We used to explain that as a convenience story — &quot;you don&apos;t need five tools.&quot; Fine.…</description><pubDate>Sat, 07 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;At Outseta we have billing, auth, email, support, and CRM under one roof. We used to explain that as a convenience story — &amp;quot;you don&apos;t need five tools.&amp;quot; Fine. True. But not exactly exciting.&lt;/p&gt;
&lt;p&gt;Then on a recent episode of Slow&amp;amp;Steady I was exploring how Outseta&apos;s all-in-one nature could be a real differentiator when it comes to AI agents:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;In this era of AI-enabled business operations, it can be a very big differentiator that we actually have your billing data, we have your accounts, we have your email list, and we have your emails and we have your support documentation. We have all of that.&amp;quot;
&amp;lt;cite&amp;gt;🎧 Me on &lt;a href=&quot;https://slowandsteadypodcast.com/236?#t=21:39&quot;&gt;Slow &amp;amp; Steady 236@21:39 (February 2026)&lt;/a&gt; ↓&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&amp;quot;100%&amp;quot; height=&amp;quot;180&amp;quot; frameborder=&amp;quot;no&amp;quot; scrolling=&amp;quot;no&amp;quot; seamless=&amp;quot;&amp;quot; src=&amp;quot;https://share.transistor.fm/e/0ec939c2?#t=21:39&amp;quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;Later that week Geoff shared &lt;a href=&quot;https://x.com/DavidOndrej1/status/2019126831761572169&quot;&gt;this article&lt;/a&gt; in our team chat. The argument: value is moving upward into the agent layer and downward into the data layer. Everything in the middle gets crushed. Build at the data layer, become the system of record, and you become irreplaceable.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Agents come and go, data layer is forever.
&amp;lt;cite&amp;gt;&lt;a href=&quot;https://x.com/DavidOndrej1/status/2019126831761572169&quot;&gt;David Ondrej&lt;/a&gt;&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Outseta doesn&apos;t need to become a system of record. We are a system of record 🥳&lt;/p&gt;
&lt;h2&gt;What that actually looks like&lt;/h2&gt;
&lt;p&gt;When your business data is spread across five tools, an agent needs five connections, five auth flows, and has to match up records across all of them. When it&apos;s all in one place, you can point the agent to your one tool and ask:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;Did my newsletter on topic X lead to more sales?&amp;quot;&lt;/strong&gt; Who opened, who clicked, crossed with who converted after. That answer lives at the intersection of your email data and your billing data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;Which customers are at risk of churning?&amp;quot;&lt;/strong&gt; Login frequency, billing status, support tickets, email engagement. All at once. A customer who stopped opening emails, filed two support tickets, and had a failed payment last week? That&apos;s a signal you only see when the data lives together.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;What are trial users struggling with that our onboarding emails don&apos;t cover?&amp;quot;&lt;/strong&gt; Support tickets from people in their first 14 days, crossed with what your onboarding sequence actually addresses. The gaps show up fast when an agent can see both sides.&lt;/p&gt;
&lt;h2&gt;But the real thing is having it act&lt;/h2&gt;
&lt;p&gt;Tell it to draft a campaign based on the topics that actually converted. Have it pause billing for the at-risk customer, send a check-in email, and flag the account for support. Ask it to rewrite your onboarding sequence to cover the gaps your support tickets keep revealing.&lt;/p&gt;
&lt;p&gt;Or let it do all of that on its own. An agent watching your data continuously, adjusting campaigns while you sleep, catching churn signals before you notice, rewriting onboarding emails every time a new pattern shows up in support 🙈🙉🙊&lt;/p&gt;
&lt;p&gt;That&apos;s what a system of record unlocks. Not just a convenient place to keep your data. A foundation your agents can actually build on.&lt;/p&gt;
&lt;p&gt;It looks like the convenience story is becoming the agent story.&lt;/p&gt;
&lt;p&gt;Time will tell ⌛&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ No perfect MCP needed, Jean-Claude will figure it out</title><link>https://queen.raae.codes/2026-02-07-just-ship-the-mcp/</link><guid isPermaLink="true">https://queen.raae.codes/2026-02-07-just-ship-the-mcp/</guid><description>In what became AI episode #2 I asked Benedikt if he had shipped the Userlist MCP server. Kind of. OAuth flows are working. But the schemas are hard to…</description><pubDate>Sat, 07 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In what became AI episode #2 I asked Benedikt if he had shipped the &lt;a href=&quot;https://userlist.com/?via=queen&quot;&gt;Userlist&lt;/a&gt; MCP server.&lt;/p&gt;
&lt;p&gt;Kind of. OAuth flows are working. But the schemas are hard to automatically generate, so I&apos;ve been looking into replacing the current serializer library.&lt;/p&gt;
&lt;p&gt;And I was like hold up, hold up!&lt;/p&gt;
&lt;p&gt;Does your API send back proper error messages? You know, if I send a too long title when scheduling a broadcast, does it tell me it&apos;s too long?&lt;/p&gt;
&lt;p&gt;Yeah. It does.&lt;/p&gt;
&lt;p&gt;Then ship it.&lt;/p&gt;
&lt;p&gt;These models are genuinely good at figuring things out now. When our friend Jean-Claude hits an error, he will read the error message and try again. You might be on the hook for more tokens than necessary, but he will get it right eventually. And if you are lucky he remembers how to do it right the next time 🤯&lt;/p&gt;
&lt;p&gt;Turns out it&apos;s not just me saying this. The &lt;a href=&quot;https://modelcontextprotocol.io/specification/2025-11-25/server/tools#error-handling&quot;&gt;MCP spec&lt;/a&gt; itself says tool execution errors &amp;quot;contain actionable feedback that language models can use to self-correct and retry with adjusted parameters.&amp;quot;&lt;/p&gt;
&lt;p&gt;And the folks using MCP servers for marketing right now? They&apos;re not sitting there watching each API call:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;They&apos;re just YOLOing things and sending them off into subagents. So if it takes four tries to get this broadcast up, they&apos;re not even gonna see that. They&apos;re just gonna see it when it&apos;s done.&amp;quot;
&amp;lt;cite&amp;gt;🎧 Me on &lt;a href=&quot;https://slowandsteadypodcast.com/236?#t=33:07&quot;&gt;Slow &amp;amp; Steady 236@33:07 (February 2026)&lt;/a&gt; ↓&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&amp;quot;100%&amp;quot; height=&amp;quot;180&amp;quot; frameborder=&amp;quot;no&amp;quot; scrolling=&amp;quot;no&amp;quot; seamless=&amp;quot;&amp;quot; src=&amp;quot;https://share.transistor.fm/e/0ec939c2?#t=33:07&amp;quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;I&apos;m not saying schemas don&apos;t matter. They do! Rich descriptions, proper types, field constraints — all of that makes the experience smoother. But out the gate you&apos;re fine with &amp;quot;title is a string&amp;quot;. Better done, than perfect as they say at Userlist.&lt;/p&gt;
&lt;p&gt;On the topic of MCP Servers, my next step for the &lt;a href=&quot;https://outseta.com/?via=queen&quot;&gt;Outseta&lt;/a&gt; MCP is to publish it as a remote MCP server making it accessible for those not ready to npx @outseta/outseta-mcp. And yeah, I&apos;ve been stuck picking the perfect hosting setup. Same trap, different serializer library.&lt;/p&gt;
&lt;p&gt;What&apos;s the MCP you&apos;ve been not-shipping? 😬&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;li&gt;Userlist&apos;s co-founder Benedikt Deicke is Queen Raae&apos;s co-host on Slow &amp; Steady podcast.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ It Actually One-Shotted It 🥳</title><link>https://queen.raae.codes/2026-02-06-it-one-shotted-it/</link><guid isPermaLink="true">https://queen.raae.codes/2026-02-06-it-one-shotted-it/</guid><description>I copy-pasted my new Outseta + Next.js article into Cursor and said &quot;Let&apos;s create a simple Next demo using Outseta.&quot; Hit enter. And it one-shotted it.…</description><pubDate>Fri, 06 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I copy-pasted my new &lt;a href=&quot;https://go.outseta.com/support/kb/articles/B9lEKnW8/integrate-outseta-with-nextjs?via=queen&quot;&gt;Outseta + Next.js article&lt;/a&gt; into Cursor and said &amp;quot;Let&apos;s create a simple Next demo using Outseta.&amp;quot; Hit enter. And it one-shotted it. Completely correct.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./cursor-one-shot.png&quot; alt=&quot;Cursor one-shotting the Outseta + Next.js gated content article&quot;&gt;&lt;/p&gt;
&lt;p&gt;I tried this a while back. Same kind of task — integrate Outseta. And I could not for the life of me get it to work. It kept inventing a react SDK that didn&apos;t exist, even when I gave it the exact script set up I wanted in the header.&lt;/p&gt;
&lt;p&gt;But this time: &lt;strong&gt;Boom.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I looked through the code and the Outseta integration was there, the exact way I would have done it.&lt;/p&gt;
&lt;p&gt;Hear me talk about it on &lt;a href=&quot;https://slowandsteadypodcast.com/235?#t=16:19&quot;&gt;Slow &amp;amp; Steady ep. 235 (at the 16:19 mark)&lt;/a&gt; ↓&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&amp;quot;100%&amp;quot; height=&amp;quot;180&amp;quot; frameborder=&amp;quot;no&amp;quot; scrolling=&amp;quot;no&amp;quot; seamless=&amp;quot;&amp;quot; src=&amp;quot;https://share.transistor.fm/e/29d2248f?#t=16:19&amp;quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;We&apos;ve gone from &amp;quot;I think we need to change how Outseta works in order to work with AI&amp;quot; to &amp;quot;AI can actually do it the way we expect it to.&amp;quot;&lt;/p&gt;
&lt;p&gt;That&apos;s a pretty big shift.&lt;/p&gt;
&lt;p&gt;So what changed? I think it&apos;s the model. But it could be that I&apos;ve gotten better at the context stuff as well...&lt;/p&gt;
&lt;p&gt;When I was testing way back when, as in mid last year (2025), I tried all kinds of ways. Concrete step-by-step instructions, detailed specs, minimal specs, full access to &lt;a href=&quot;https://outseta.com/?via=queen&quot;&gt;Outseta&lt;/a&gt;&apos;s knowledge base, you name it. Still garbage. Now I pasted in a single knowledge base article and the AI just got it. But here&apos;s the thing...The article was written with the help of Claude 😬 and I&apos;m much more experienced prompt engineer these days. Perhaps that&apos;s what made the difference. Or a bit of both? Honestly, I&apos;m not sure.&lt;/p&gt;
&lt;p&gt;I might actually test this. Same article, same prompt, different models. See who one-shots it and who tries to download an SDK that doesn&apos;t exist.&lt;/p&gt;
&lt;p&gt;Stay tuned 🤓&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ Three render modes your Framer components should handle</title><link>https://queen.raae.codes/2025-12-09-framer-render-modes/</link><guid isPermaLink="true">https://queen.raae.codes/2025-12-09-framer-render-modes/</guid><description>If you&apos;re building code components or code overrides for Framer, you&apos;ll want to handle three different render modes: canvas, preview, and live. Canvas — The…</description><pubDate>Tue, 09 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you&apos;re building code components or code overrides for &lt;a href=&quot;https://framer.link/queen-raae&quot;&gt;Framer&lt;/a&gt;, you&apos;ll want to handle three different render modes: canvas, preview, and live.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Canvas&lt;/strong&gt; — The Framer editor. Your component is rendered as a static preview while designing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Preview&lt;/strong&gt; — Preview mode. Interactive, but still in the Framer environment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live&lt;/strong&gt; — The published site where custom code such as the Outseta script executes.&lt;/p&gt;
&lt;p&gt;Unexpectedly (at least to me 😆), Framer&apos;s &lt;code&gt;RenderTarget&lt;/code&gt; returns &amp;quot;preview&amp;quot; for both preview and live as I&apos;ve defined them. This makes sense for design, but not for more app-like functionality as I&apos;ve found out while building &lt;a href=&quot;https://outseta.com/?via=queen&quot;&gt;Outseta&lt;/a&gt; code overrides and components that depend on the execution of custom code.&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;After some trial and error, I found the most robust way to distinguish between preview and live is to check the hostname. If it includes &amp;quot;framercanvas.com&amp;quot;, it&apos;s preview, otherwise it&apos;s live.&lt;/p&gt;
&lt;p&gt;Live is intentionally the fallback mode, so if Framer changes the preview hostname the component will still work as expected in the more crucial live mode.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { RenderTarget } from &amp;quot;framer&amp;quot;;

type RenderMode = &amp;quot;canvas&amp;quot; | &amp;quot;preview&amp;quot; | &amp;quot;live&amp;quot;;

function getRenderMode(): RenderMode {
  const renderTarget = RenderTarget.current();

  if (renderTarget === RenderTarget.canvas) {
    return &amp;quot;canvas&amp;quot;;
  } else if (renderTarget === RenderTarget.preview &amp;amp;&amp;amp; window?.location.host.includes(&amp;quot;framercanvas.com&amp;quot;)) {
    return &amp;quot;preview&amp;quot;;
  } else {
    return &amp;quot;live&amp;quot;;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why not do feature checks instead?&lt;/h2&gt;
&lt;p&gt;This is a valid approach — we could check for the presence of the Outseta script and only continue with Outseta logic if present:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;if (typeof Outseta !== &amp;quot;undefined&amp;quot;) {
  // Outseta is available, do the thing
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This handles the error, but it doesn&apos;t give you control over the designer experience. With feature checks alone we&apos;re not asking &amp;quot;what experience should I provide for this mode?&amp;quot;&lt;/p&gt;
&lt;p&gt;With explicit mode handling you can, for example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add props like &lt;code&gt;showOnPreview&lt;/code&gt; to force a specific state, letting designers preview logged-in views without having to publish&lt;/li&gt;
&lt;li&gt;Write to the console in live mode when the Outseta script is missing and remove the component — but in preview mode a missing script is expected, so no need to clutter the console with warnings&lt;/li&gt;
&lt;li&gt;Skip analytics events in canvas and preview — only fire them in live mode&lt;/li&gt;
&lt;li&gt;And so on...&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Framer code runs in three modes:&lt;/strong&gt; canvas, preview, and live — each needs different behavior&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External scripts only run in live mode&lt;/strong&gt; — your code needs fallbacks for canvas and preview&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live as fallback is intentional&lt;/strong&gt; — if Framer changes their preview hostname, your published site still works&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mode checks &amp;gt; feature checks&lt;/strong&gt; — they give you control over the designer experience, not just error avoidance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What is your approach to handling render modes in Framer? Are there edge cases I haven&apos;t covered? I&apos;d love to hear about it.&lt;/p&gt;
&lt;p&gt;Happy building! 🛠️&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ Copy current URL with a Framer Code Override</title><link>https://queen.raae.codes/2025-10-27-framer-copy-button/</link><guid isPermaLink="true">https://queen.raae.codes/2025-10-27-framer-copy-button/</guid><description>I&apos;ve been browsing the Framer community forum lately as I&apos;m working on an updated Outseta Framer Plugin. There I found a question about how to add a &quot;copy…</description><pubDate>Mon, 27 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve been browsing the &lt;a href=&quot;https://framer.link/queen-raae&quot;&gt;Framer&lt;/a&gt; community forum lately as I&apos;m working on an updated &lt;a href=&quot;https://outseta.com/?via=queen&quot;&gt;Outseta&lt;/a&gt; &lt;a href=&quot;https://framer.link/outseta-plugin&quot;&gt;Framer Plugin&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There I found a &lt;a href=&quot;https://www.framer.community/c/developers/copy-current-page-link-button-cms-supported&quot;&gt;question about how to add a &amp;quot;copy link&amp;quot; code override&lt;/a&gt; and threw together a code override.&lt;/p&gt;
&lt;h2&gt;What are Code Overrides?&lt;/h2&gt;
&lt;p&gt;Code overrides are Higher Order React Components that wrap around &amp;quot;stuff&amp;quot; on the Framer canvas. So if you are a React developer, you can use code overrides to add all sorts of functionality to a Framer site.&lt;/p&gt;
&lt;p&gt;Framer is really onto something here with seperating design from functionality 🤩&lt;/p&gt;
&lt;h2&gt;The Ask&lt;/h2&gt;
&lt;p&gt;Copy the current page URL to the clipboard with a Code Override.&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;A lightweight Code Override that adds copy-to-clipboard functionality to any element.&lt;/p&gt;
&lt;h2&gt;The Code&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { forwardRef, type ComponentType } from &amp;quot;react&amp;quot;;

export function withCopyURL(Component): ComponentType {
  return forwardRef((props, ref) =&amp;gt; {
    const handleClick = async () =&amp;gt; {
      try {
        const currentURL = window.location.href;
        await navigator.clipboard.writeText(currentURL);
        console.log(&amp;quot;URL copied to clipboard:&amp;quot;, currentURL);
      } catch (err) {
        console.error(&amp;quot;Failed to copy URL:&amp;quot;, err);
      }
    };

    return &amp;lt;Component ref={ref} {...props} onClick={handleClick} style={{ cursor: &amp;quot;pointer&amp;quot; }} /&amp;gt;;
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;How to Use It&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;In Framer, open the &lt;strong&gt;Assets&lt;/strong&gt; panel and go to the &lt;strong&gt;Code&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;+&lt;/strong&gt; button to create a new Code Override file and name it&lt;/li&gt;
&lt;li&gt;Paste the code above into the file&lt;/li&gt;
&lt;li&gt;Select any element on your canvas (a button, icon, or text layer)&lt;/li&gt;
&lt;li&gt;In the properties panel, find &lt;strong&gt;Code Override&lt;/strong&gt; and select &lt;code&gt;withCopyURL&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That&apos;s it! Now when someone clicks that element, the current page URL gets copied to their clipboard.&lt;/p&gt;
&lt;p&gt;You can add visual feedback like hover states or tap animation in the Framer canvas.&lt;/p&gt;
&lt;h2&gt;Going Further&lt;/h2&gt;
&lt;p&gt;You could extend this Code Override to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Show a toast notification after copying (&amp;quot;Link copied!&amp;quot;)&lt;/li&gt;
&lt;li&gt;Copy a custom URL instead of the current one (great for referral links)&lt;/li&gt;
&lt;li&gt;Track copy events in your analytics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; 
&lt;strong&gt;PS:&lt;/strong&gt; The Clipboard API works in all modern browsers, but it requires a secure context (HTTPS). It&apos;ll work fine on your published Framer site, but might act up on localhost without HTTPS.&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ UPDATE requires SELECT Row Level Security (RLS) permissions in Postgres/Supabase</title><link>https://queen.raae.codes/2025-05-10-rls-update-select/</link><guid isPermaLink="true">https://queen.raae.codes/2025-05-10-rls-update-select/</guid><description>When implementing Row Level Security (RLS) in PostgreSQL for Feedback Fort made with React + Supabase + Outseta edition, I spent way way way too long figuring…</description><pubDate>Sat, 10 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When implementing Row Level Security (RLS) in PostgreSQL for &lt;a href=&quot;https://outseta-supabase-react-feedback-fort.netlify.app/&quot;&gt;Feedback Fort&lt;/a&gt; made with React + Supabase + &lt;a href=&quot;https://outseta.com/?via=queen&quot;&gt;Outseta&lt;/a&gt; edition, I spent way way way too long figuring out why soft deleting a vote by updating &lt;code&gt;deleted_at&lt;/code&gt; kept stating:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;new row violates row-level security policy for table &amp;quot;vote&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;😩😩😩&lt;/p&gt;
&lt;p&gt;Let&apos;s hope this article saves you some time!&lt;/p&gt;
&lt;h2&gt;My setup&lt;/h2&gt;
&lt;p&gt;I had configured two separate policies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- For viewing active votes
CREATE POLICY &amp;quot;Anyone can view active votes&amp;quot;
  ON vote FOR SELECT
  USING (deleted_at IS NULL);

-- For updating votes
CREATE POLICY &amp;quot;Users can update their votes&amp;quot;
  ON vote FOR UPDATE
  USING (auth.jwt() -&amp;gt;&amp;gt; &apos;sub&apos; = outseta_person_uid)
  WITH CHECK (auth.jwt() -&amp;gt;&amp;gt; &apos;sub&apos; = outseta_person_uid);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In my (still beginners) mental model of RLS, the &lt;code&gt;UPDATE&lt;/code&gt; policy would only be used when updating the vote, and the &lt;code&gt;SELECT&lt;/code&gt; policy would be used when selecting the vote.&lt;/p&gt;
&lt;h2&gt;The issue&lt;/h2&gt;
&lt;p&gt;So I googled, and found some intersting new knowleged by reading the &lt;a href=&quot;https://www.postgresql.org/docs/current/sql-createpolicy.html&quot;&gt;PostgreSQL docs&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;Typically an &lt;code&gt;UPDATE&lt;/code&gt; command also needs to read data from columns in the relation being updated (e.g., in a &lt;code&gt;WHERE&lt;/code&gt; clause or a &lt;code&gt;RETURNING&lt;/code&gt; clause, or in an expression on the right hand side of the &lt;code&gt;SET&lt;/code&gt; clause). In this case, &lt;code&gt;SELECT&lt;/code&gt; rights are also required on the relation being updated, and the appropriate &lt;code&gt;SELECT&lt;/code&gt; or &lt;code&gt;ALL&lt;/code&gt; policies will be applied in addition to the &lt;code&gt;UPDATE&lt;/code&gt; policies.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Or in other words: &lt;strong&gt;UPDATE operations implicitly require SELECT access to the rows being modified&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;But the user did in fact have &lt;code&gt;SELECT&lt;/code&gt; permissions on the vote as when initating the update, &lt;code&gt;deleted_at&lt;/code&gt; value was NULL.&lt;/p&gt;
&lt;p&gt;So why was the update failing?&lt;/p&gt;
&lt;p&gt;😠😠😠&lt;/p&gt;
&lt;p&gt;After much googling and head scratching, and testing it seems like &lt;strong&gt;SELECT policies must be valid BOTH before AND after an UPDATE operation&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I have not succeeded in finding any documentation on this, but numerous posts on Stack Overflow and Github issues seem to suggest this is the case.&lt;/p&gt;
&lt;p&gt;But Claude gave me this explanation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PostgreSQL requires SELECT policies to be valid both before and after an UPDATE operation to maintain transactional consistency. This ensures you can always see the results of your own modifications and prevents situations where rows would &apos;disappear&apos; mid-transaction.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I would love to get a more authoritative source on this, if you have one, please let me know (queen@raae.codes)!&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;The solution in my case was to change the SELECT policy to allow anyone to see active votes and all their votes regardless of status.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Modified SELECT policy to see your own votes regardless of deleted status
CREATE POLICY &amp;quot;Users can view their votes&amp;quot;
  ON vote FOR SELECT
  USING (deleted_at IS NULL OR auth.jwt() -&amp;gt;&amp;gt; &apos;sub&apos; = outseta_person_uid);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This policy combines two visibility rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Anyone can see active votes (where &lt;code&gt;deleted_at IS NULL&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Users can always see their votes (where &lt;code&gt;auth.jwt() -&amp;gt;&amp;gt; &apos;sub&apos; = outseta_person_uid&lt;/code&gt;), regardless of deletion status&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The complete Feedback Fort code is available on &lt;a href=&quot;https://github.com/outseta/outseta-supabase-react-feedback-fort&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Key Takeaway&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;RLS is easy to get started with, but also easy to mess up.&lt;/li&gt;
&lt;li&gt;Read the docs 🤪&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But more to the point of this article, remember:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;UPDATE operations first need to SELECT the rows they&apos;ll modify&lt;/li&gt;
&lt;li&gt;After an UPDATE, you must still have SELECT access to the modified row&lt;/li&gt;
&lt;/ol&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ How to use JWT from any auth provider with Supabase RLS</title><link>https://queen.raae.codes/2025-05-01-supabase-exchange/</link><guid isPermaLink="true">https://queen.raae.codes/2025-05-01-supabase-exchange/</guid><description>Supabase provides Row Level Security (RLS) as a way to control access to your data. RLS makes it possible to query data from your client without an API layer.…</description><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Supabase provides Row Level Security (RLS) as a way to control access to your data. RLS makes it possible to query data from your client without an API layer. But what if you want to use your existing authentication system, instead of Supabase Auth?&lt;/p&gt;
&lt;p&gt;This is something our &lt;a href=&quot;https://outseta.com/?via=queen&amp;amp;utm_source=queen&amp;amp;utm_medium=blog&amp;amp;utm_campaign=supabase-exchange&quot;&gt;Outseta&lt;/a&gt; users struggled with. I was pretty sure it was possible, but it took me some time to come up with the solution. I realised it could be used with any JWT-based auth provider, so I thought I&apos;d share the solution here as well.&lt;/p&gt;
&lt;p&gt;&amp;lt;aside class=&amp;quot;notice&amp;quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;I&apos;ve also created a full &lt;a href=&quot;https://outseta-supabase-react-feedback-fort.netlify.app/&quot;&gt;React + Supabase + Outseta demo app&lt;/a&gt; that you can use as a starting point for your own project. Full source code is available on &lt;a href=&quot;https://github.com/outseta/outseta-supabase-react-feedback-fort&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&amp;lt;/aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;You have an existing authentication system that issues JWTs, but you want to leverage Supabase&apos;s Row Level Security (RLS) features that expect Supabase-signed tokens.&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;Exchange your authentication provider&apos;s JWT for a Supabase-signed JWT, then use the latter for all Supabase operations.&lt;/p&gt;
&lt;h2&gt;How It Works&lt;/h2&gt;
&lt;p&gt;The token exchange must happen server-side and follows these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Verify the original JWT&lt;/strong&gt; using your auth provider&apos;s public key or JWKS endpoint&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create a new JWT&lt;/strong&gt; with additional claims required by Supabase&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sign the new JWT&lt;/strong&gt; with your Supabase JWT Secret&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use the Supabase-signed JWT&lt;/strong&gt; in subsequent requests&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Implementation Steps&lt;/h2&gt;
&lt;h3&gt;1. Set Up Your Authentication Provider&lt;/h3&gt;
&lt;p&gt;And take note of the shape for the JWT payload they provide, typical it will include things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sub&lt;/code&gt;: The unique ID of the authenticated user&lt;/li&gt;
&lt;li&gt;&lt;code&gt;email&lt;/code&gt;: User&apos;s email address&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;: User&apos;s name&lt;/li&gt;
&lt;li&gt;&lt;code&gt;org_id&lt;/code&gt;: User&apos;s organisation ID&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For an example, check out the &lt;a href=&quot;https://go.outseta.com/support/kb/articles/XQYMXqQP/the-jwt-access-token?utm_source=queen&amp;amp;utm_medium=blog&amp;amp;utm_campaign=supabase-exchange&quot;&gt;Outseta JWT docs&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;2. Create an Exchange Function&lt;/h3&gt;
&lt;p&gt;Deploy a server-side function that handles the token exchange. This can be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A Supabase Edge Function&lt;/li&gt;
&lt;li&gt;An API route in your application server&lt;/li&gt;
&lt;li&gt;A serverless function somewhere&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&apos;ll use a Supabase Edge Function for this example, but the same principles apply to any server-side function.&lt;/p&gt;
&lt;p&gt;You&apos;ll need the following environment variables for the Edge Function (found in Supabase Console under Edge Functions -&amp;gt; Secrets):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SUPABASE_JWT_SECRET&lt;/code&gt;: Your Supabase JWT secret (found in the Supabase Console under Project Settings -&amp;gt; Data API)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AUTH_JWKS_URL&lt;/code&gt;: The JWKS URL for your auth provider (found in the auth provider&apos;s docs)
&lt;ul&gt;
&lt;li&gt;or &lt;code&gt;AUTH_PUBLIC_KEY&lt;/code&gt;: The public key for your auth provider (found in the auth provider&apos;s docs)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To deploy the function, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;supabase functions deploy exchange --no-verify-jwt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or upload the function from the Supabase Console making sure to disable the &amp;quot;Enforce JWT Verification&amp;quot; option.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;--no-verify-jwt&lt;/code&gt; flag is essential because this endpoint is requested with the JWTs from your external auth provider, not a Supabase-signed tokens. Without this flag, Supabase would automatically reject these requests as it would try to verify them as Supabase JWTs.&lt;/p&gt;
&lt;p&gt;Here&apos;s a sample exchange function using Supabase Edge Functions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// File: /functions/exchange/index.ts
// Deploy with: supabase functions deploy exchange --no-verify-jwt

import * as jose from &amp;quot;https://deno.land/x/jose@v4.14.4/index.ts&amp;quot;;

const corsHeaders = {
  &amp;quot;Access-Control-Allow-Origin&amp;quot;: &amp;quot;*&amp;quot;,
  &amp;quot;Access-Control-Allow-Headers&amp;quot;: &amp;quot;authorization, x-client-info, apikey, content-type&amp;quot;,
};

Deno.serve(async (req) =&amp;gt; {
  if (req.method === &amp;quot;OPTIONS&amp;quot;) {
    return new Response(&amp;quot;ok&amp;quot;, { headers: corsHeaders });
  }

  // Get the original JWT from the Authorization header
  const authHeader = req.headers.get(&amp;quot;Authorization&amp;quot;);
  const originalJwt = authHeader?.split(&amp;quot; &amp;quot;)[1] || &amp;quot;&amp;quot;;

  try {
    // OPTION 1: Verify with JWKS URL
    const JWKS = jose.createRemoteJWKSet(new URL(Deno.env.get(&amp;quot;AUTH_JWKS_URL&amp;quot;)));

    // OPTION 2: Verify with public key
    // const publicKey = await jose.importSPKI(Deno.env.get(&amp;quot;AUTH_PUBLIC_KEY&amp;quot;), &amp;quot;RS256&amp;quot;);

    // Verify the token
    const { payload } = await jose.jwtVerify(originalJwt, JWKS);

    // Add the required role claim if not already present for a valid Supabase JWT
    payload.role = &amp;quot;authenticated&amp;quot;; // Required by Supabase

    // Add or modify any other claims you need for RLS policies
    // payload.some_claim = &amp;quot;some claim&amp;quot;;

    // Sign with Supabase JWT secret
    const supabaseSecret = new TextEncoder().encode(Deno.env.get(&amp;quot;SUPABASE_JWT_SECRET&amp;quot;));

    const supabaseJwt = await new jose.SignJWT(payload)
      .setProtectedHeader({ alg: &amp;quot;HS256&amp;quot;, typ: &amp;quot;JWT&amp;quot; })
      .setIssuer(&amp;quot;supabase&amp;quot;)
      .setIssuedAt(payload.iat)
      .setExpirationTime(payload.exp || &amp;quot;&amp;quot;)
      .sign(supabaseSecret);

    // Return the Supabase JWT
    return new Response(JSON.stringify({ supabaseJwt }), {
      headers: { ...corsHeaders, &amp;quot;Content-Type&amp;quot;: &amp;quot;application/json&amp;quot; },
      status: 200,
    });
  } catch (error) {
    console.error(&amp;quot;JWT verification failed:&amp;quot;, error.message);
    return new Response(JSON.stringify({ error: &amp;quot;Invalid token&amp;quot; }), {
      headers: { ...corsHeaders, &amp;quot;Content-Type&amp;quot;: &amp;quot;application/json&amp;quot; },
      status: 401,
    });
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Use the Exchanged JWT with Supabase Client&lt;/h3&gt;
&lt;p&gt;The most elegant way to use the exchanged JWT is to configure the Supabase client with a custom accessToken handler that automatically exchanges tokens:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { createClient } from &amp;quot;@supabase/supabase-js&amp;quot;;

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;

// Create Supabase client with automatic token exchange
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  accessToken: async (fallbackToken) =&amp;gt; {
    // Get the original JWT from your auth provider
    const originalJwt = getAuthProviderToken(); // Replace with your auth provider&apos;s method

    if (!originalJwt) {
      return null; // No token available
    }

    // Exchange it for a Supabase token
    const supabaseJwt = await exchangeToken(originalJwt);
    return supabaseJwt || fallbackToken;
  },
});

// Function to exchange the original JWT for a Supabase JWT
async function exchangeToken(originalJwt) {
  // Perhaps add some caching here to avoid unnecessary exchanges,
  // only need to exchange if originalJwt has changed
  try {
    console.log(&amp;quot;Exchanging token for Supabase access&amp;quot;);
    const response = await fetch(`${supabaseUrl}/functions/v1/exchange`, {
      method: &amp;quot;POST&amp;quot;,
      headers: {
        Authorization: `Bearer ${originalJwt}`,
        &amp;quot;Content-Type&amp;quot;: &amp;quot;application/json&amp;quot;,
      },
    });

    if (!response.ok) {
      throw new Error(`Token exchange failed: ${response.status}`);
    }

    const { supabaseJwt } = await response.json();
    return supabaseJwt;
  } catch (error) {
    console.error(&amp;quot;Error exchanging token:&amp;quot;, error);
    return null;
  }
}

// Example usage - just use the supabase client normally!
// The token exchange happens automatically behind the scenes
const { data, error } = await supabase.from(&amp;quot;my_table&amp;quot;).select(&amp;quot;*&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Creating RLS Policies with the Exchanged JWT&lt;/h2&gt;
&lt;p&gt;Supabase makes the decoded JWT available in RLS policies through the built-in &lt;code&gt;auth.jwt()&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Example: Only allow users to read their own records
CREATE POLICY &amp;quot;Users can read their own data&amp;quot; ON my_table
  FOR SELECT
  -- Using auth.jwt() -&amp;gt;&amp;gt; &apos;sub&apos;instead of auth.uid() as you could with Supabase auth
  USING (auth.jwt() -&amp;gt;&amp;gt; &apos;sub&apos; = user_id);

-- Example: Organization-based access if you have an org_id claim
CREATE POLICY &amp;quot;Users can access organization data&amp;quot; ON org_resources
  FOR ALL
  USING (auth.jwt() -&amp;gt;&amp;gt; &apos;org_id&apos; = organization_id);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Setting Default Values from JWT Claims&lt;/h2&gt;
&lt;p&gt;You can also use JWT claims as default values for table columns:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Example: Automatically set the user_id when a record is created
ALTER TABLE my_table
  ALTER COLUMN user_id SET DEFAULT auth.jwt() -&amp;gt;&amp;gt; &apos;sub&apos;;

-- Example: Set organization_id from JWT claim
ALTER TABLE org_resources
  ALTER COLUMN organization_id SET DEFAULT auth.jwt() -&amp;gt;&amp;gt; &apos;org_id&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Important Considerations&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt;: Always verify the original JWT on the server side before exchanging it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claims Mapping&lt;/strong&gt;: Transfer all relevant claims from the original JWT to the Supabase JWT&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expiration&lt;/strong&gt;: Preserve the original token&apos;s expiration time in the Supabase token&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error Handling&lt;/strong&gt;: Handle verification failures gracefully&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By implementing this token exchange pattern, you can continue using your existing authentication system while taking full advantage of Supabase&apos;s powerful RLS capabilities. This approach gives you the flexibility to use any JWT-based auth provider that does not have a built-in Supabase integration, such as &lt;a href=&quot;https://outseta.com/?via=queen&amp;amp;utm_source=queen&amp;amp;utm_medium=blog&amp;amp;utm_campaign=supabase-exchange&quot;&gt;Outseta&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Happy building! 🚀&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ Select post variant with a Code Override in Framer</title><link>https://queen.raae.codes/2024-03-18-variant-framer/</link><guid isPermaLink="true">https://queen.raae.codes/2024-03-18-variant-framer/</guid><description>This week I&apos;m building a Framer x Outseta Patreon clone live on stream together with Damien, a designer I know from the Interwebz. We&apos;ll design the site in…</description><pubDate>Mon, 18 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This week I&apos;m building a &lt;a href=&quot;https://www.youtube.com/@outseta/streams&quot;&gt;Framer x Outseta Patreon&lt;/a&gt; clone live on stream together with Damien, a designer I know from the Interwebz.&lt;/p&gt;
&lt;p&gt;We&apos;ll design the site in &lt;a href=&quot;https://framer.link/queen-raae&quot;&gt;Framer&lt;/a&gt;, use &lt;a href=&quot;https://outseta.com/?via=queen&quot;&gt;Outseta&lt;/a&gt; for authentication and protection of members-only content, and stitch it all together with Framer&apos;s Code Overrides.&lt;/p&gt;
&lt;p&gt;The feature we tackled today was to show a different post-item variant based on the link to the post. We&apos;ll configure &lt;a href=&quot;https://go.outseta.com/support/kb/categories/rQVZLeQ6/protected-content&quot;&gt;Outseta to protect&lt;/a&gt; all pages starting with &amp;quot;/posts/locked-&amp;quot; from unauthenticated visitors, so it makes sense to use the same mechanism for selecting the &amp;quot;locked&amp;quot; design of the post.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./primary-locked.png&quot; alt=&quot;Primary and Locked variants&quot;&gt;&lt;/p&gt;
&lt;h2&gt;The Code Override&lt;/h2&gt;
&lt;p&gt;The Code Override checks if the link of the post starts with &amp;quot;/posts/locked-&amp;quot;. If the link does, it selects the &amp;quot;locked&amp;quot; variant, and if not it selects the &amp;quot;primary&amp;quot; variant:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export function withCorrectVariant(Component): ComponentType {
  return (props) =&amp;gt; {
    const { link } = props;
    const variant = link.startsWith(&amp;quot;/posts/locked-&amp;quot;) ? &amp;quot;Locked&amp;quot; : &amp;quot;Primary&amp;quot;;
    return &amp;lt;Component {...props} variant={variant} /&amp;gt;;
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That Code Override name is not my best work, please help me out by &lt;a href=&quot;https://twitter.com/intent/tweet?text=%40raae%20a%20better%20name%20would%20be&quot;&gt;suggesting a better name&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Gotcha: the &lt;code&gt;link&lt;/code&gt; property is the pathname sans domain to the, and it starts with a &amp;quot;/&amp;quot;.&lt;/p&gt;
&lt;h2&gt;The Result&lt;/h2&gt;
&lt;p&gt;The items linking to pages starting with &amp;quot;/posts/locked-&amp;quot; now show the &amp;quot;locked&amp;quot; variant and the rest show the &amp;quot;primary&amp;quot; variant.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./the-result.png&quot; alt=&quot;Primary and Locked post items with demo content&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Extending Framer functionality with Code Overrides is such a superpower, especially if you are someone who can spell variant right on the first try...I cannot as becomes evident if you watch the full stream on &lt;a href=&quot;https://www.youtube.com/live/s0eXaQr26Xs?si=_ToBcXuhKtfP72G2&quot;&gt;YouTube&lt;/a&gt;.&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ Join us in building MixPod</title><link>https://queen.raae.codes/2023-11-02-join-us-mixpod/</link><guid isPermaLink="true">https://queen.raae.codes/2023-11-02-join-us-mixpod/</guid><description>We may have been sailing in silence...but our adventures through the high seas of the World Wide Web never stops 🌊 A new idea for a treasure hunt has been…</description><pubDate>Thu, 02 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;We may have been sailing in silence...but our adventures through the high seas of the World Wide Web never stops 🌊&lt;/p&gt;
&lt;p&gt;A new idea for a treasure hunt has been brewing: MixPod 🎧&lt;/p&gt;
&lt;p&gt;Our maiden voyage commences tomorrow (Saturday) &lt;a href=&quot;https://www.youtube.com/live/ZWhzBS0PJQg&quot;&gt;at 11:00 CET on YouTube&lt;/a&gt;.&lt;br&gt;
Join us as we plot the course and create the MixPod Treasure Map.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/live/ZWhzBS0PJQg&quot;&gt;&lt;img src=&quot;./youtube-thumb.png&quot; alt=&quot;YouTube Thumbnail for stream&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What&apos;s MixPod, you wonder?&lt;br&gt;
Think of it as mixtapes but for podcast episodes.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./miss-mixpod.jpeg&quot; alt=&quot;Miss MixPod with a cassette as her head with the tagline - make a playlist of podcast episodes for your friend&quot;&gt;&lt;/p&gt;
&lt;p&gt;All the best,&lt;br&gt;
Queen Raae&lt;/p&gt;
&lt;p&gt;PS: The project is made possible by &lt;a href=&quot;https://outseta.com/?via=queen&amp;amp;utm_source=raae.codes&amp;amp;utm_medium=email&amp;amp;utm_campaign=2023-11-02-we-are-back&quot;&gt;Outseta&lt;/a&gt;, and we&apos;re hopeful more sponsors will join us along the way.&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ How to send an email in 10 days from your app?</title><link>https://queen.raae.codes/2023-09-19-how-to-send-emails/</link><guid isPermaLink="true">https://queen.raae.codes/2023-09-19-how-to-send-emails/</guid><description>How would you send an email 10 days from now in your app? Below is a cleaned-up version of my response to Aditya Malani when he asked this question on x-bird…</description><pubDate>Tue, 19 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;How would you send an email 10 days from now in your app?&lt;/p&gt;
&lt;p&gt;Below is a cleaned-up version of my response to &lt;a href=&quot;https://twitter.com/Sniper2804/status/1701883884936548765&quot;&gt;Aditya Malani&lt;/a&gt; when he asked this question on x-bird app.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Drip Campaigns:&lt;/strong&gt; Platforms like &lt;a href=&quot;https://userlist.com/&quot;&gt;Userlist&lt;/a&gt;, &lt;a href=&quot;https://convertkit.com/&quot;&gt;ConvertKit&lt;/a&gt;, or &lt;a href=&quot;https://outseta.com/?via=queen&quot;&gt;Outseta&lt;/a&gt; offer capabilities to set up drip campaigns with delays you may trigger from your app.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Email Scheduling:&lt;/strong&gt; Services like like &lt;a href=&quot;https://sendgrid.com/&quot;&gt;SendGrid&lt;/a&gt; and &lt;a href=&quot;https://mailchimp.com/features/transactional-email&quot;&gt;Mailchimp Transactional&lt;/a&gt; let you schedule emails in the future.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cron job:&lt;/strong&gt; Set up a cron job (for Node, there is &lt;a href=&quot;https://github.com/node-cron/node-cron&quot;&gt;node-cron&lt;/a&gt;) to periodically check the time since the event and send an email through providers such as &lt;a href=&quot;https://resend.com/&quot;&gt;Resend&lt;/a&gt; when the elapsed time hits 10 days.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Queues:&lt;/strong&gt; Last but not least, you may use a queue solution with delay capabilities like &lt;a href=&quot;https://www.rabbitmq.com/&quot;&gt;RabbitMQ&lt;/a&gt; or &lt;a href=&quot;https://www.inngest.com/&quot;&gt;Inngest&lt;/a&gt; with SendGrid, Resend etc.&lt;/p&gt;
&lt;p&gt;Any I missed? Any your favorite way?&lt;/p&gt;
&lt;p&gt;All the best,&lt;br&gt;
Queen Raae&lt;/p&gt;
&lt;p&gt;PS: Remember to move your Gatsby Cloud sites soon (mostly a reminder to myself).&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;li&gt;Userlist&apos;s co-founder Benedikt Deicke is Queen Raae&apos;s co-host on Slow &amp; Steady podcast.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ Keeping things around until the session ends</title><link>https://queen.raae.codes/2023-05-11-session-storage/</link><guid isPermaLink="true">https://queen.raae.codes/2023-05-11-session-storage/</guid><description>As mentioned when talking about search params, an Outseta customer wanted to pass along the UTM search params to the Outseta SignUp widget so that a visitor…</description><pubDate>Thu, 11 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As mentioned when talking about &lt;a href=&quot;/2023-05-06-url-search-params/&quot;&gt;search params&lt;/a&gt;, an &lt;a href=&quot;http://www.outseta.com?via=queen&quot;&gt;Outseta&lt;/a&gt; customer wanted to pass along the UTM search params to the Outseta SignUp widget so that a visitor who came in through &lt;em&gt;https://example.com?utm_source=facebook&amp;amp;utm_medium=paid_social&amp;amp;utm_campaign=summer_sale&lt;/em&gt; gets attributed to the summer sale paid Facebook ad.&lt;/p&gt;
&lt;p&gt;The vistor might not sign up at the first page they land on, so it makes sense to keep the UTM search params around for the entirety of the session.&lt;/p&gt;
&lt;p&gt;Vanilla JS again has our backs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var params = new URL(window.location).searchParams;
var utmSource = params.get(&amp;quot;utm_source&amp;quot;);
var utmCampaign = params.get(&amp;quot;utm_campaign&amp;quot;);
var utmMedium = params.get(&amp;quot;utm_medium&amp;quot;);

if (UtmSource || UtmCampaign || UtmMedium) {
  try {
    sessionStorage.setItem(&amp;quot;UtmSource&amp;quot;, utmSource);
    sessionStorage.setItem(&amp;quot;UtmCampaign&amp;quot;, utmCampaign);
    sessionStorage.setItem(&amp;quot;UtmMedium&amp;quot;, utmMedium);
  } catch (error) {
    console.warn(&amp;quot;Could not save UTM params to session storage&amp;quot;);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The try block is essential because turning off cookies in a browser also disables session storage as it can be used for the same purposes (tracking people) as you see here. So you want to make sure you handle that.&lt;/p&gt;
&lt;p&gt;In addition, if you add this script to every page, the values will be overridden by empty strings on subsequent page visits. Therefore we add that if statement to ensure we have all the values we expect a visitor landing on a page from our campaign to have. Make sure to tweak this to your use case!&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;All the best,&lt;br&gt;
Queen Raae&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ BBQ Season 🍢 And this week&apos;s schedule</title><link>https://queen.raae.codes/2023-05-08-this-week/</link><guid isPermaLink="true">https://queen.raae.codes/2023-05-08-this-week/</guid><description>Last week&apos;s BBQ turned out great even though we had to dress up quite a bit 🤣 We&apos;ll be hosting a BBQ for WebDevs in Oslo on Tuesday, May 23rd at 18.00, so if…</description><pubDate>Mon, 08 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Last week&apos;s BBQ turned out great even though we had to dress up quite a bit 🤣&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://twitter.com/raae/status/1652992561844215808&quot;&gt;&lt;img src=&quot;./bbq-tweet.png&quot; alt=&quot;At one point there was even sun ☀️ However I didn&apos;t manage to capture that 😬&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We&apos;ll be hosting a BBQ for WebDevs in Oslo on Tuesday, &lt;strong&gt;May 23rd at 18.00&lt;/strong&gt;, so if you live in town or are headed for NDC {Oslo}, we&apos;d love to see you! Wanna come, reply to this email ↩️&lt;/p&gt;
&lt;p&gt;Tomorrow we&apos;ll interview &lt;a href=&quot;https://twitter.com/AnfibiaCreativa&quot;&gt;Natalia&lt;/a&gt;, database extraordinaire at Microsoft, for the Data in the Wild podcast.&lt;/p&gt;
&lt;h2&gt;This week&lt;/h2&gt;
&lt;p&gt;🔴 🦋 &lt;a href=&quot;https://www.youtube.com/live/P1UCS86fsP4&quot;&gt;Data Model Chat with Natalia from Microsoft&lt;/a&gt; · &lt;strong&gt;Data in the wild · A podcast from Xata — the serverless database platform&lt;/strong&gt; &lt;br&gt;
— Tuesday, May 9th @ 12:00 CEST&lt;/p&gt;
&lt;p&gt;🔴 🏴‍☠️ &lt;a href=&quot;https://www.youtube.com/live/S_fwn8S7UHU&quot;&gt;Visualising Outseta customers on the map using React Leaflet + Gatsby&lt;/a&gt; · &lt;strong&gt;JamstackPirates&lt;/strong&gt;&lt;br&gt;
— Thursday, May 11th @ 19:00 CEST&lt;/p&gt;
&lt;h2&gt;Next month&lt;/h2&gt;
&lt;p&gt;🎒 Attending &lt;a href=&quot;https://reactnorway.com/&quot;&gt;React Norway&lt;/a&gt; · Let me know if you are going!&lt;br&gt;
— Friday, June 16th&lt;/p&gt;
&lt;p&gt;🔴 🖼️ &lt;a href=&quot;https://www.youtube.com/@Cloudinary/streams&quot;&gt;Cloudinary DevJam&lt;/a&gt; · &lt;strong&gt;Cloudinary&lt;/strong&gt;&lt;br&gt;
— Wednesday, June 21st @ 20:00 CEST&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;All the best,&lt;br&gt;
Queen Raae&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae and family worked on several projects for Xata.&lt;/li&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ How to properly handle search (query) params in javascript</title><link>https://queen.raae.codes/2023-05-06-url-search-params/</link><guid isPermaLink="true">https://queen.raae.codes/2023-05-06-url-search-params/</guid><description>An Outseta customer wanted to pass along the UTM search params to the Outseta SignUp widget so that a visitor who came in through…</description><pubDate>Sat, 06 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;An &lt;a href=&quot;http://www.outseta.com?via=queen&quot;&gt;Outseta&lt;/a&gt; customer wanted to pass along the UTM search params to the Outseta SignUp widget so that a visitor who came in through &lt;em&gt;https://example.com?utm_source=facebook&amp;amp;utm_medium=paid_social&amp;amp;utm_campaign=summer_sale&lt;/em&gt; gets attributed to the summer sale paid Facebook ad.&lt;/p&gt;
&lt;p&gt;Search, or query params, is the information after the &lt;code&gt;?&lt;/code&gt; in a URL such as &lt;code&gt;utm_source=facebook&amp;amp;utm_medium=paid_social&amp;amp;utm_campaign=summer_sale&lt;/code&gt; in our example.&lt;/p&gt;
&lt;p&gt;UTM is a set of params commonly used for tracking marketing efforts, but you can add anything here to suit your needs.&lt;/p&gt;
&lt;p&gt;I did some quick googling and came over many creative solutions like separating out the search params by splitting on &lt;code&gt;?&lt;/code&gt; etc.&lt;/p&gt;
&lt;p&gt;However, vanilla JS supports this use case out of the box 🤯&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const url = new URL(
  &amp;quot;https://example.com?utm_source=facebook&amp;amp;utm_medium=paid_social&amp;amp;utm_campaign=summer_sale&amp;quot;
);

console.log(
  url.searchParams.get(&amp;quot;utm_source&amp;quot;),
  url.searchParams.get(&amp;quot;utm_medium&amp;quot;),
  url.searchParams.get(&amp;quot;utm_campaign&amp;quot;)
);

// Output: facebook paid_social summer_sale
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;/2022-05-10-new-url/&quot;&gt;&lt;code&gt;URL&lt;/code&gt; is the constructor&lt;/a&gt; you should reach for every time you deal with URLs, and the &lt;code&gt;searchParams&lt;/code&gt; we are accessing here conforms to the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams&quot;&gt;&lt;code&gt;URLSearchParams&lt;/code&gt; interface with methods such as &lt;code&gt;has&lt;/code&gt;, &lt;code&gt;sort&lt;/code&gt;, &lt;code&gt;getAll&lt;/code&gt; and more&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Never try to deal with URLs yourself; JS got your back!&lt;/p&gt;
&lt;p&gt;You&apos;ll also need to keep the values arround in &lt;a href=&quot;/2023-05-11-session-storage/&quot;&gt;session storage&lt;/a&gt; to make use of them later.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;All the best,&lt;br&gt;
Queen Raae&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title>📝 ✨ ~ Sometimes you POST to update, sometimes you PUT</title><link>https://queen.raae.codes/2023-04-14-post-put/</link><guid isPermaLink="true">https://queen.raae.codes/2023-04-14-post-put/</guid><description>I once spent what felt like an eternity not understanding why updating a ConvertKit subscriber resulted in a 404. Luckily half moon was watching and let me…</description><pubDate>Fri, 14 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I once spent what felt like an eternity not understanding why updating a ConvertKit subscriber resulted in a 404. Luckily &lt;em&gt;half moon&lt;/em&gt; was watching and let me know I needed to use PUT, not POST for this.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/live/ZNhD4pXZOhI?feature=share&amp;amp;t=2506&quot;&gt;&lt;img src=&quot;./screenshot-put-post.png&quot; alt=&quot;It need to be PUT, not POST&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Using PUT is technically the correct use of the HTTP methods:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The difference between POST and PUT is that PUT requests are idempotent. That is, calling the same PUT request multiple times will always produce the same result. In contrast, calling a POST request repeatedly have side effects of creating the same resource multiple times.&lt;/p&gt;
&lt;p&gt;&amp;lt;cite&amp;gt;&lt;a href=&quot;https://www.w3schools.com/tags/ref_httpmethods.asp&quot;&gt;W3Schools&lt;/a&gt;&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But not what my favorite API in the whole wide world, the Stripe API and many others, does. Stripe use POST for both creation and updating.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;POST /v1/customers
POST /v1/customers/:id
&amp;lt;cite&amp;gt;&lt;a href=&quot;https://www.w3schools.com/tags/ref_httpmethods.asp&quot;&gt;Stripe API Docs&lt;/a&gt;&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But once burned, I always double-check, which saved us when using the &lt;a href=&quot;http://www.outseta.com?via=queen&quot;&gt;Outseta&lt;/a&gt; API on yesterday&apos;s rum-fueled treasure hunt. The Outseta API also uses PUT for updates.&lt;/p&gt;
&lt;p&gt;So this is your reminder to double-check that HTTP Method before pursuing other explanations for failing updates.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;All the best,&lt;br&gt;
Queen Raae&lt;/p&gt;
&lt;br/&gt;&lt;ul&gt;&lt;li&gt;Queen Raae works part-time as Outseta&apos;s Developer Advocate.&lt;/li&gt;&lt;/ul&gt;</content:encoded></item></channel></rss>