<?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>Forem: siegerts</title>
    <description>The latest articles on Forem by siegerts (@siegerts).</description>
    <link>https://forem.com/siegerts</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F424003%2Fee361bd1-6389-4720-9e06-dfa72626ceb1.jpeg</url>
      <title>Forem: siegerts</title>
      <link>https://forem.com/siegerts</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/siegerts"/>
    <language>en</language>
    <item>
      <title>Hitting Your 1 Rep Max with AI</title>
      <dc:creator>siegerts</dc:creator>
      <pubDate>Tue, 03 Mar 2026 05:00:00 +0000</pubDate>
      <link>https://forem.com/siegerts/hit-your-1-rep-max-with-ai-27ck</link>
      <guid>https://forem.com/siegerts/hit-your-1-rep-max-with-ai-27ck</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo6i846k4wwwl4ok3vb4e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo6i846k4wwwl4ok3vb4e.png" alt="ai 1 rep max" width="800" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the gym, your 1 rep max is the most weight you can lift for a single repetition. It's not about how many reps and sets you can do. It's about finding the absolute edge of what you're capable of. Just once, in the moment after you've warmed up or just have a day that you're "feeling it".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's what AI gives you for thinking. Without any of the limitations.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We block ourselves. I could do this but it will take time. Or, we know the possible but are afraid to pursue because it &lt;em&gt;might actually work&lt;/em&gt;...and then what? It will take too much time to validate, too much time to continue. So we don't start.&lt;/p&gt;

&lt;p&gt;This isn't just for building tech. That what-if question you've had, the one that would take extreme focus and time to even try. What if you could attempt it now? And then the next one. Design a piece of furniture. Create a game. Build a business. It can translate into the physical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is quite honestly one of the wildest times.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I went to school for Economics. I had some sense of HTML (from MySpace pages) but I used to stall on ideas because I didn't know how to execute them. I learned to program to create. Plain and simple. I wanted to build my ideas. And that took years.&lt;/p&gt;

&lt;p&gt;Now, that gap between having an idea and being able to act on it is almost gone. You don't need to learn a whole new skillset to start. You just need to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rep ranges
&lt;/h2&gt;

&lt;p&gt;If you've spent any time in a gym, you're aware of the concept of rep ranges. The number of repetitions you do with a given weight determines the type of training effect you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;12-15+ reps&lt;/strong&gt;: Light weight, high volume. You could do this all day. It keeps you moving but it doesn't change you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8-12 reps&lt;/strong&gt;: Moderate weight. This is where you build muscle. The work that makes you grow over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3-5 reps&lt;/strong&gt;: Heavy weight, low reps. You're building raw capacity. Each rep demands focus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 rep&lt;/strong&gt;: Your max. The most you can possibly lift. One attempt. Full effort. You find out what you're actually capable of. Sometime you fall over after 😄.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most people spend their entire working life in the 12-15 rep range. Comfortable volume. Repeating what they already know. Answering emails. Putting together the same deck. Shipping the same type of feature. Running the same playbook. It's productive but it never tests the ceiling.&lt;/p&gt;

&lt;p&gt;AI lets you move down the rep range without compromising risk. You can attempt the 1 rep max without the fear of failure. You can try something you never thought you could do, knowing that if it doesn't work, it's not a big deal. You can iterate quickly and learn from the attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  How most people use it
&lt;/h2&gt;

&lt;p&gt;Right now (at least for this week because things are moving so fast) most people outside of the tech bubble use AI for the easy, repetitive stuff. Summarize this, draft that, auto-complete the thing you've done a hundred times. Nothing changes. Same work, less effort.&lt;/p&gt;

&lt;p&gt;Some go further. They explore a domain they haven't worked in. Pick up a new tool or framework. Dig into something they've been meaning to learn. That's where I think real growth happens.&lt;/p&gt;

&lt;p&gt;Fewer still use it to take on harder problems than they normally would. The project that felt out of reach. The decision they would have deferred to someone else.&lt;/p&gt;

&lt;p&gt;And then there's the attempt you genuinely don't know if you can pull off. A non-technical founder building their own product. A designer prototyping something they'd normally hire a dev for. A marketer who's been outsourcing analysis learning to do it themselves. A backend developer shipping a polished UI.&lt;/p&gt;

&lt;p&gt;Or, someone that wants to design a piece of furniture because "hey, why not?".&lt;/p&gt;

&lt;p&gt;Most people never get past the first level. I think the unlock is realizing you can. The time cost to try is almost nothing now, especially when you would have never tried at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shift
&lt;/h2&gt;

&lt;p&gt;Once I got into this pattern, something changed for me. I stopped treating new ideas like R&amp;amp;D projects that needed justification and started just...doing them. You're not convincing yourself to "just do it" but rather accepting that you can.&lt;/p&gt;

&lt;p&gt;A friend of mine, sales exec, told me recently "I just iterate until it looks right and does what I want it to do." That's it. No architecture planning, no analysis paralysis. Just start.&lt;/p&gt;

&lt;p&gt;I think a lot of this is psychological. What would you do if you could hit your 1 rep max whenever you wanted? Some people embrace that. Others are paralyzed by the influx of decisions they were never faced with before. What do I build? What do I try? What's worth my time now that everything feels possible?&lt;/p&gt;

&lt;p&gt;Work that took days, months, and sometimes years can happen in minutes. That's a lot to process.&lt;/p&gt;

&lt;h2&gt;
  
  
  "I could never make that"
&lt;/h2&gt;

&lt;p&gt;This is the phrase I hear the most. From non-technical people who have ideas but assume the gap between idea and execution is uncrossable. From people who've been in the same role, same tools, same output for years. From technical people who've settled into the same stack, same architecture, same ceiling.&lt;/p&gt;

&lt;p&gt;The gap was real. It's not anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I think about it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1s58jym58jbvrcn5g94r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1s58jym58jbvrcn5g94r.png" alt="diagram" width="800" height="721"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Rethinking, not outsourcing
&lt;/h2&gt;

&lt;p&gt;The failure mode is obvious though. Most people get AI and immediately use it to make the easy stuff easier. Same work, less effort. That's not growth, that's automation.&lt;/p&gt;

&lt;p&gt;I think the distinction that matters is automation vs. augmentation. Automation keeps you where you are. Augmentation pushes what you can do. Rethinking means looking at the thing you've never attempted and saying...&lt;em&gt;what if I could?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To be clear, AI doesn't do it for you. It's more like having someone there who gives you the confidence to try something bigger. It's there if you fail. So you're willing to fail. Not "do this for me" but "I want to try something I've never done. Stay close."&lt;/p&gt;

&lt;p&gt;Honestly, it's a psychological shift.&lt;/p&gt;

&lt;p&gt;None of this works if you don't actually try though. AI doesn't generate ambition or curiosity. But if you bring those, it'll let you do more than you thought you could.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;During the time that I was writing this, I also built a working mobile app with a framework (SwiftUI) that I had no experience with. I've never touched it before...&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;(all my opinions btw and maybe just a reflection of how I'm feeling...today)&lt;/p&gt;

</description>
      <category>learning</category>
      <category>agenticai</category>
      <category>creativity</category>
    </item>
    <item>
      <title>Crawling Pages with Infinite Scroll using Scrapy and Playwright</title>
      <dc:creator>siegerts</dc:creator>
      <pubDate>Thu, 08 Aug 2024 15:33:55 +0000</pubDate>
      <link>https://forem.com/siegerts/crawling-pages-with-infinite-scroll-using-scrapy-and-playwright-cfk</link>
      <guid>https://forem.com/siegerts/crawling-pages-with-infinite-scroll-using-scrapy-and-playwright-cfk</guid>
      <description>&lt;p&gt;When crawling websites with &lt;a href="https://scrapy.org/" rel="noopener noreferrer"&gt;Scrapy&lt;/a&gt; you'll quickly come across all sorts of scenarios that require you to get creative or interact with the page that you're trying to scrape. One of these scenarios is when you need to crawl an infinite scroll page. This type of website page loads more content as you scroll down the page like a social media feed.&lt;/p&gt;

&lt;p&gt;There is definitely more than one way to crawl these types of pages. One way I recently approached this was to continue scrolling until the page length stopped increasing (i.e. scroll to the bottom). This post steps through this process.&lt;/p&gt;

&lt;p&gt;This post assumes that you have a Scrapy project set up, running, and a Spider that you can modify and run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Playwright with Scrapy
&lt;/h2&gt;

&lt;p&gt;This integration uses the &lt;a href="https://github.com/scrapy-plugins/scrapy-playwright" rel="noopener noreferrer"&gt;scrapy-playwright&lt;/a&gt; plugin to integrate &lt;a href="https://playwright.dev/python/" rel="noopener noreferrer"&gt;Playwright for Python&lt;/a&gt; with Scrapy. Playwright is a headless browser automation library used to interact with web pages and extract data.&lt;/p&gt;

&lt;p&gt;I've been using &lt;a href="https://github.com/astral-sh/uv" rel="noopener noreferrer"&gt;uv&lt;/a&gt; for Python package installation and management. &lt;/p&gt;

&lt;p&gt;Then, I use virtual environments right from uv with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv venv 
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the &lt;code&gt;scrapy-playwright&lt;/code&gt; plugin and Playwright with the following command into your virtual environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv pip &lt;span class="nb"&gt;install &lt;/span&gt;scrapy-playwright
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the browser you want to use with Playwright. For example, to install Chromium, you can run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also install other browsers like Firefox if needed. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The below Scrapy code and Playwright integration have only been tested with Chromium. &lt;/p&gt;

&lt;p&gt;Update the &lt;code&gt;settings.py&lt;/code&gt; file or the &lt;code&gt;custom_settings&lt;/code&gt; attribute in the spider to include the &lt;code&gt;DOWNLOAD_HANDLERS&lt;/code&gt; and &lt;code&gt;PLAYWRIGHT_LAUNCH_OPTIONS&lt;/code&gt; settings.&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="c1"&gt;# settings.py
&lt;/span&gt;&lt;span class="n"&gt;TWISTED_REACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;twisted.internet.asyncioreactor.AsyncioSelectorReactor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;DOWNLOAD_HANDLERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;PLAYWRIGHT_LAUNCH_OPTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# optional for CORS issues
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--disable-web-security&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--disable-features=IsolateOrigins,site-per-process&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;# optional for debugging
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headless&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;PLAYWRIGHT_LAUNCH_OPTIONS&lt;/code&gt; you can set the &lt;code&gt;headless&lt;/code&gt; option to &lt;code&gt;False&lt;/code&gt; to have the browser instance open and watch the process run. This is good for debugging and building out the initial scraper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dealing with CORS Issues
&lt;/h2&gt;

&lt;p&gt;I pass in the additional &lt;code&gt;args&lt;/code&gt; to disable web security and isolate origins. This is useful when you are crawling sites that have CORS issues.&lt;/p&gt;

&lt;p&gt;For example, there may be situations when required JavaScript assets are not loaded or network requests are not made because of CORS. You can isolate this faster by checking the browser console for errors if certain page actions (like clicking a button) are not working as expected but everything else is.&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PLAYWRIGHT_LAUNCH_OPTIONS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--disable-web-security&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--disable-features=IsolateOrigins,site-per-process&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headless&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Crawling Infinite Scroll Pages
&lt;/h2&gt;

&lt;p&gt;This is an example of a spider that crawls an infinite scroll page. The spider scrolls the page by 700 pixels and waits for 750ms for the request to complete. The spider will continue to scroll until it reaches the bottom of the page indicated by the the scroll position not changing as it goes through the loop.&lt;/p&gt;

&lt;p&gt;I'm modifying the settings in the spider itself using  &lt;code&gt;custom_settings&lt;/code&gt; to keep the settings in one place. You can also add these settings to the &lt;code&gt;settings.py&lt;/code&gt; file.&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="c1"&gt;# /&amp;lt;project&amp;gt;/spiders/infinite_scroll.py
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;scrapy&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scrapy.spiders&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CrawlSpider&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scrapy.selector&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Selector&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InfinitePageSpider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CrawlSpider&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Spider to crawl an infinite scroll page
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;infinite_scroll&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;allowed_domains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;allowed_domain&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;start_urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;start_url&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;custom_settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TWISTED_REACTOR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;twisted.internet.asyncioreactor.AsyncioSelectorReactor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DOWNLOAD_HANDLERS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PLAYWRIGHT_LAUNCH_OPTIONS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--disable-web-security&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--disable-features=IsolateOrigins,site-per-process&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headless&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start_requests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;scrapy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_urls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;playwright_include_page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;


    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;playwright_page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_default_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;last_position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window.scrollY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="c1"&gt;# scroll by 700 while not at the bottom
&lt;/span&gt;                    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window.scrollBy(0, 700)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;750&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# wait for 750ms for the request to complete
&lt;/span&gt;                    &lt;span class="n"&gt;current_position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window.scrollY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_position&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;last_position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reached the bottom of the page.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="k"&gt;break&lt;/span&gt;

                    &lt;span class="n"&gt;last_position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_position&lt;/span&gt;

            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;pass&lt;/span&gt;

            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Getting content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Parsing content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;selector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Extracting links&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;links&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;//a[contains(@href, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&amp;lt;link-pattern&amp;gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)]//@href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Found &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; links...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Yielding links&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing that I've learned is that no two pages or sites are the same so you may need to adjust the scroll amount and wait time to account for the page and also any latency in the network round trips for the requests to complete. You can dynamically adjust this programmatically by checking the scroll position and the time it takes for the request to complete.&lt;/p&gt;

&lt;p&gt;On the page load, I'm waiting a bit longer for the assets to load and the page to render. The Playwright page is passed to the &lt;code&gt;parse&lt;/code&gt; callback method in the &lt;code&gt;response.meta&lt;/code&gt; object. This is used to interact with the page and scroll the page. This is specified in the &lt;code&gt;scrapy.Request&lt;/code&gt; arguments with the &lt;code&gt;playwright=True&lt;/code&gt; and &lt;code&gt;playwright_include_page=True&lt;/code&gt; options.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start_requests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;scrapy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_urls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;playwright_include_page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This spider will &lt;a href="https://playwright.dev/docs/api/class-page#page-evaluate" rel="noopener noreferrer"&gt;scroll the page with &lt;code&gt;page.evaluate&lt;/code&gt;&lt;/a&gt; and the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollBy" rel="noopener noreferrer"&gt;scrollBy() JavaScript method&lt;/a&gt; by 700 pixels and then wait for 750ms for the request to complete. Then, the Playwright page content is copied to a Scrapy selector, and extract the links from the page. The links are then yielded to the Scrapy pipeline to continue processing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F28w2if4tgvb08fnlkql4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F28w2if4tgvb08fnlkql4.png" alt="Infinite Scroll Scrapy Playwright" width="632" height="837"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;small&gt;Infinite Scroll Scrapy Playwright&lt;/small&gt;



&lt;p&gt;For situations where the page requests begin to load duplicate content, you can add a check to see if the content has already been loaded and then break out of the loop. Or, if you have an idea of the number of scroll loads, you can add a counter to break out of the loop after a certain number of scrolls plus/minus a buffer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Infinite Scroll with an Element Click
&lt;/h2&gt;

&lt;p&gt;It's also possible that the page may have an element that you can scroll to (i.e. "Load more") that will trigger the next set of content to load. You can use the &lt;code&gt;page.evaluate&lt;/code&gt; method to scroll to the element and then click it to load the next set of content.&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="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;//button[contains(., &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Load more&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Load more&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; button found.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="n"&gt;is_disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_disabled&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_disabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Button is disabled.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scroll_into_view_if_needed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;750&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method is useful when you know the page has a button that will load the next set of content. You can also use this method to click on other elements that will trigger the next set of content to load. The &lt;code&gt;scroll_into_view_if_needed&lt;/code&gt; method will scroll the button or element into view if it is not already visible on the page. This is one of those scenarios when you will want to double-check the page actions with &lt;code&gt;headless=False&lt;/code&gt; to see if the button is being clicked and the content is being loaded as expected before running a full crawl.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: As mentioned above, confirm that the page assets(&lt;code&gt;.js&lt;/code&gt;) are loading correctly and that the network requests are being made so that the button (or element) is mounted and clickable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Web crawling is a case-by-case scenario and you will need to adjust the code to fit the page that you are trying to scrape. The above code is a starting point to get you going with crawling infinite scroll pages with Scrapy and Playwright. &lt;/p&gt;

&lt;p&gt;Hopefully, this helps to get you unblocked! 🙌&lt;/p&gt;

&lt;p&gt;Subscribe to get my latest content by email -&amp;gt; &lt;a href="https://siegerts.ck.page/74222e7e60" rel="noopener noreferrer"&gt;Newsletter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>scrapy</category>
      <category>playwright</category>
    </item>
    <item>
      <title>Building and deploying a Slack app with Python, Bolt, and AWS Amplify</title>
      <dc:creator>siegerts</dc:creator>
      <pubDate>Mon, 15 Jul 2024 14:21:04 +0000</pubDate>
      <link>https://forem.com/siegerts/building-and-deploying-a-slack-app-with-python-bolt-and-aws-amplify-2b57</link>
      <guid>https://forem.com/siegerts/building-and-deploying-a-slack-app-with-python-bolt-and-aws-amplify-2b57</guid>
      <description>&lt;p&gt;Slack apps and bots are a handy way to automate and streamline repetitive tasks. Slack's &lt;a href="https://api.slack.com/start/building/bolt-python" rel="noopener noreferrer"&gt;Bolt&lt;/a&gt; framework consolidates the different mechanisms to capture and interact with Slack events into a single library. &lt;/p&gt;

&lt;p&gt;From the &lt;a href="https://api.slack.com/tools/bolt" rel="noopener noreferrer"&gt;Slack Bolt documentation&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;All flavors of Bolt are equipped to help you build apps and integrations with our most commonly used features.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Install your app using OAuth 2.0&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Receive real time events and messages with the Events API&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Compose rich, interactive messages with Block Kit&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Respond to slash commands, shortcuts, and interactive messages&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Use a wide library of Web API methods&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;



&lt;p&gt;In my experience, this has made it straight forward to combine functionality instead of having multiple apps all handling different elements of Slack's API ecosystem.&lt;/p&gt;

&lt;p&gt;There is a &lt;strong&gt;ton&lt;/strong&gt; that you can do combining this library with the Amplify toolchain. So, let's create an app using Python 🐍!&lt;/p&gt;

&lt;p&gt;We'll create a simple Slack app that's triggered when the &lt;code&gt;/start-process&lt;/code&gt; slash command is run from within Slack.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F07bihwwy8cvz1v2m3cd2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F07bihwwy8cvz1v2m3cd2.png" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;h1&gt;
  
  
  Set up your Amplify project
&lt;/h1&gt;

&lt;p&gt;This is the standard procedure. The project name used is &lt;code&gt;slackamplify&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;? Enter a name &lt;span class="k"&gt;for &lt;/span&gt;the project slackamplify
The following configuration will be applied:

Project information
| Name: slackamplify
| Environment: dev
| Default editor: Visual Studio Code
| App &lt;span class="nb"&gt;type&lt;/span&gt;: javascript
| Javascript framework: none
| Source Directory Path: src
| Distribution Directory Path: dist
| Build Command: npm run-script build
| Start Command: npm run-script start

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  Add a REST API with a Lambda function
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.amplify.aws/cli/restapi" rel="noopener noreferrer"&gt;REST API&lt;/a&gt; is the lifeline of the Slack app. This API will be called each time the slash command is invoked from within Slack.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;amplify add api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the API path, I use a base path (&lt;code&gt;/&lt;/code&gt;) below. If you something specific here, make sure to append that path to your base URL endpoint when configuring the Slack app below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;? Please &lt;span class="k"&gt;select &lt;/span&gt;from one of the below mentioned services: REST
? Provide a friendly name &lt;span class="k"&gt;for &lt;/span&gt;your resource to be used as a label &lt;span class="k"&gt;for &lt;/span&gt;this category &lt;span class="k"&gt;in &lt;/span&gt;the project: slackpython
? Provide a path &lt;span class="o"&gt;(&lt;/span&gt;e.g., /book/&lt;span class="o"&gt;{&lt;/span&gt;isbn&lt;span class="o"&gt;})&lt;/span&gt;: /
? Choose a Lambda &lt;span class="nb"&gt;source &lt;/span&gt;Create a new Lambda &lt;span class="k"&gt;function&lt;/span&gt;
? Provide an AWS Lambda &lt;span class="k"&gt;function &lt;/span&gt;name: slackpythonfunction
? Choose the runtime that you want to use: Python
Only one template found - using Hello World by default.

Available advanced settings:
- Resource access permissions
- Scheduled recurring invocation
- Lambda layers configuration

? Do you want to configure advanced settings? No
? Do you want to edit the &lt;span class="nb"&gt;local &lt;/span&gt;lambda &lt;span class="k"&gt;function &lt;/span&gt;now? No
Successfully added resource slackpythonfunction locally.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  Set up the function's virtual environment
&lt;/h2&gt;

&lt;p&gt;On to the function!&lt;/p&gt;

&lt;p&gt;But first, we need to update the function's Python virtual environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install the Python dependencies
&lt;/h3&gt;

&lt;p&gt;The next step is &lt;strong&gt;very important&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;Change into the function's directory!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd &amp;lt;project&amp;gt;/amplify/backend/function/&amp;lt;function-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To install dependencies, you'll need to first activate the Python virtual environment (below) for this function. To do this, you need to be in the function directory (step above). I've seen this trip folks up when folks actually install dependencies into the global Python installation and not into the correct virtual environment. The Amplify CLI will will &lt;em&gt;prep&lt;/em&gt; your function directory for &lt;a href="https://pipenv.pypa.io/en/latest/" rel="noopener noreferrer"&gt;Pipenv&lt;/a&gt; if you select the Python runtime for your function during the &lt;code&gt;amplify add api&lt;/code&gt; prompts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TIP&lt;/strong&gt;&lt;br&gt;
A quick reminder - Pipenv differs slightly from other Python virtual environment tooling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pipenv shell&lt;/code&gt; activates the environment&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exit&lt;/code&gt; deactivates the environment&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  Activate the virtual environment
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipenv shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I like to think of each Amplify function as it's own isolated environment where the code (i.e. functionality) pairs &lt;code&gt;1:1&lt;/code&gt; with the dependencies within that environment. And for each function, you can have your own isolated Python virtual environment environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Footh7o9qky6x5hfb8yra.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Footh7o9qky6x5hfb8yra.png" width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to share dependencies, or code, across functions then you'll want to check out the &lt;a href="https://docs.amplify.aws/cli/function/layers" rel="noopener noreferrer"&gt;Lambda Layers&lt;/a&gt; capability in the Amplify CLI to see if that fits your use case.&lt;/p&gt;

&lt;p&gt;Now, install the required dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipenv &lt;span class="nb"&gt;install &lt;/span&gt;slack_bolt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing the first dependency, you will see a &lt;strong&gt;Pipfile.lock&lt;/strong&gt; file in the directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipenv &lt;span class="nb"&gt;install &lt;/span&gt;python-lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;Pipfile&lt;/strong&gt; will now list the above dependencies that you installed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  [[source]]
  name = "pypi"
  url = "https://pypi.org/simple"
  verify_ssl = true
&lt;span class="err"&gt;
&lt;/span&gt;  [dev-packages]
&lt;span class="err"&gt;
&lt;/span&gt;  [packages]
  src = {editable = true, path = "./src"}
&lt;span class="gi"&gt;+ slack-bolt = "*"
+ python-lambda = "*"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  [requires]
  python_version = "3.8"
&lt;span class="err"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  Update the Lambda function
&lt;/h2&gt;

&lt;p&gt;At this point, we'll use the the placeholder Lambda function from the &lt;a href="https://slack.dev/bolt-python/concepts#lazy-listeners" rel="noopener noreferrer"&gt;Example with AWS Lambda&lt;/a&gt; in the &lt;strong&gt;Bolt&lt;/strong&gt; documentation. It may be collapsed &lt;em&gt;and at the bottom of the page&lt;/em&gt;, so double check that you're looking at the right snippet.&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="c1"&gt;# reference example from: https://slack.dev/bolt-python/concepts#lazy-listeners
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slack_bolt&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;App&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slack_bolt.adapter.aws_lambda&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SlackRequestHandler&lt;/span&gt;

&lt;span class="c1"&gt;# process_before_response must be True when running on FaaS
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process_before_response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;respond_to_slack_within_3_seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Usage: /start-process (description here)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accepted! (task: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_long_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# longer than 3 seconds
&lt;/span&gt;    &lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Completed! (task: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/start-process&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;
    &lt;span class="n"&gt;ack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;respond_to_slack_within_3_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# responsible for calling `ack()`
&lt;/span&gt;    &lt;span class="n"&gt;lazy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;run_long_process&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# unable to call `ack()` / can have multiple functions
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;slack_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SlackRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;slack_handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I recommend reading through &lt;a href="https://github.com/slackapi/bolt-python/issues/335#issuecomment-837095097" rel="noopener noreferrer"&gt;this GitHub&lt;/a&gt; thread to get a better understanding of &lt;code&gt;process_before_response&lt;/code&gt;, &lt;code&gt;ack()&lt;/code&gt;, and &lt;code&gt;lazy=&lt;/code&gt; in the code above. These elements make it possible to run this an application like this in a serverless environment without the need for a persistent long-running server.&lt;/p&gt;



&lt;h3&gt;
  
  
  Adjusting the function's IAM policy
&lt;/h3&gt;

&lt;p&gt;You'll need to add the adjust policy below for your Lambda function as mentioned in the &lt;strong&gt;Bolt&lt;/strong&gt; docs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VisualEditor0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"lambda:InvokeFunction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"lambda:GetFunction"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To do so, adjust the &lt;code&gt;lambdaexecutionpolicy&lt;/code&gt; in the &lt;strong&gt;/amplify/backend/function/&amp;lt;function-name&amp;gt;/&amp;lt;function-name&amp;gt;-cloudformation-template.json&lt;/strong&gt; to add the above statement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;"lambdaexecutionpolicy": {
    "DependsOn": ["LambdaExecutionRole"],
    "Type": "AWS::IAM::Policy",
    "Properties": {
        "PolicyName": "lambda-execution-policy",
        "Roles": [{ "Ref": "LambdaExecutionRole" }],
        "PolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Action": ["logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"],
                    "Resource": { "Fn::Sub": [ "arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*", { "region": {"Ref": "AWS::Region"}, "account": {"Ref": "AWS::AccountId"}, "lambda": {"Ref": "LambdaFunction"}} ]}
                 },
&lt;span class="gi"&gt;+                {
+                   "Sid": "VisualEditor0",
+                   "Effect": "Allow",
+                   "Action": [
+                      "lambda:InvokeFunction",
+                      "lambda:GetFunction"
+                   ],
+                   "Resource": "*"
+               }
&lt;/span&gt;             ]
         }
     }
 }
&lt;span class="err"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  Push the API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;amplify push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And create the resources 🚀&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

| Category | Resource name       | Operation | Provider plugin   |
| -------- | ------------------- | --------- | ----------------- |
| Function | slackpythonfunction | Create    | awscloudformation |
| Api      | slackpython         | Create    | awscloudformation |
? Are you sure you want to continue? (Y/n) Yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once this is deployed, the URL can be used in the next step (Create the Slack app).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;REST API endpoint: https://&amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;.execute-api.&amp;lt;region&amp;gt;.amazonaws.com/dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now have 90% of the Slack app deployed. The app needs environment variables to be complete but we can only get those once we create a new app in Slack. So, let's do that. &lt;/p&gt;



&lt;h2&gt;
  
  
  Create the Slack app
&lt;/h2&gt;

&lt;p&gt;Now, you'll need to head over to Slack to create (if it doesn't exist) and link the app to the Amplify API.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://api.slack.com/apps" rel="noopener noreferrer"&gt;https://api.slack.com/apps&lt;/a&gt; and &lt;strong&gt;Create new app&lt;/strong&gt; &amp;gt; &lt;strong&gt;From scratch&lt;/strong&gt;. Use the endpoint from the API above!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0xk86gqje6oggj4y9ajh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0xk86gqje6oggj4y9ajh.png" width="800" height="595"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Select Name &amp;amp; workspace&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmb4eux070lj9mczmb2jc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmb4eux070lj9mczmb2jc.png" width="800" height="739"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Basic information&lt;/strong&gt; &amp;gt; &lt;strong&gt;Add features &amp;amp; functionality&lt;/strong&gt; &amp;gt; &lt;strong&gt;Slash commands&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmb4eux070lj9mczmb2jc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmb4eux070lj9mczmb2jc.png" width="800" height="739"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Basic information&lt;/strong&gt; &amp;gt; &lt;strong&gt;Install your app&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuj91x56p4vtrjto8p63t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuj91x56p4vtrjto8p63t.png" width="800" height="606"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;h2&gt;
  
  
  Update the Lambda function's environment variables
&lt;/h2&gt;

&lt;p&gt;Slack will generate the required tokens and secrets that you'll need to populate into your function with environment variables once the app and command are set up.&lt;/p&gt;

&lt;p&gt;Add the below variable/token pairs to the function in Lambda. To add environment variables, go to &lt;strong&gt;Configuration&lt;/strong&gt; &amp;gt; &lt;strong&gt;Environment variables&lt;/strong&gt; in the AWS Console for the Lambda function.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SLACK_SIGNING_SECRET&lt;/code&gt;: &lt;strong&gt;Basic information&lt;/strong&gt; &amp;gt; &lt;strong&gt;App credentials&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SLACK_BOT_TOKEN&lt;/code&gt;     : &lt;strong&gt;OAuth &amp;amp; Permissions&lt;/strong&gt; &amp;gt; starts with &lt;code&gt;xoxo-&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F11q1agxue46ynpajuqnd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F11q1agxue46ynpajuqnd.png" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;h2&gt;
  
  
  Test the command in Slack
&lt;/h2&gt;

&lt;p&gt;Awesome! Now let's test the slash command. &lt;/p&gt;

&lt;p&gt;It shows in the command menu.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F07bihwwy8cvz1v2m3cd2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F07bihwwy8cvz1v2m3cd2.png" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;And...the command runs as expected when submitted along with some data.&lt;/p&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzit64416dvp6i5fgefs5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzit64416dvp6i5fgefs5.png" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;There you have it! A Slack app running in Lambda created with AWS Amplify running in a serverless context.&lt;/p&gt;

&lt;p&gt;I'd definitely take a look at the other ways that you can &lt;a href="https://github.com/SlackAPI/bolt-python" rel="noopener noreferrer"&gt;listen and respond&lt;/a&gt; to messages. My favorite? Responding to reactions...&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="nd"&gt;@app.message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:wave:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;say_hello&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;say&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hi there, &amp;lt;@&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;TIP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you start listening for other events, you'll need to verify your endpoint configuration. For example, some events require that the endpoint URL end with &lt;code&gt;/slack/events&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;Hope that helps! 🚀&lt;/p&gt;

&lt;p&gt;If you have any questions or feedback, feel free to reach out on &lt;a href="https://x.com/siegerts" rel="noopener noreferrer"&gt;X @siegerts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Subscribe to get my latest content by email -&amp;gt; &lt;a href="https://siegerts.ck.page/74222e7e60" rel="noopener noreferrer"&gt;Newsletter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>awsamplify</category>
      <category>python</category>
      <category>aws</category>
      <category>slackbot</category>
    </item>
    <item>
      <title>Verifying Lemon Squeezy Subscription Webhooks in Cloudflare Workers</title>
      <dc:creator>siegerts</dc:creator>
      <pubDate>Thu, 27 Jun 2024 15:33:55 +0000</pubDate>
      <link>https://forem.com/siegerts/verifying-lemon-squeezy-subscription-webhooks-in-cloudflare-workers-469j</link>
      <guid>https://forem.com/siegerts/verifying-lemon-squeezy-subscription-webhooks-in-cloudflare-workers-469j</guid>
      <description>&lt;p&gt;I've been using Lemon Squeezy to handle &lt;a href="https://docs.lemonsqueezy.com/help/products/subscriptions" rel="noopener noreferrer"&gt;subscription billing&lt;/a&gt; in several projects. Lemon Squeezy offers an easy way to manage subscriptions and payments, similar to the DX of Stripe, but includes tax remittance and compliance as a Merchant of Record (MoR). So, using Lemon Squeezy helps offload the complexities of tax, compliance, and dispute handling. Additionally, I like the idea of having the built-in email marketing and affiliate portal as optional features.&lt;/p&gt;

&lt;p&gt;This is a high-level overview of how I use &lt;a href="https://developers.cloudflare.com/workers/" rel="noopener noreferrer"&gt;Cloudflare Workers&lt;/a&gt; and &lt;a href="https://developers.cloudflare.com/d1/" rel="noopener noreferrer"&gt;D1&lt;/a&gt; to verify and save Lemon Squeezy subscription webhook data. I wanted to do this &lt;strong&gt;without&lt;/strong&gt; enabling &lt;a href="https://developers.cloudflare.com/workers/runtime-apis/nodejs/" rel="noopener noreferrer"&gt;Node.js compatibility&lt;/a&gt; in the Worker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This assumes that you have an understanding of Cloudflare Workers, D1, and have set up a &lt;a href="https://docs.lemonsqueezy.com/guides/developer-guide/webhooks#from-the-dashboard" rel="noopener noreferrer"&gt;Lemon Squeezy webhook endpoint&lt;/a&gt; for your product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhook Management and Integration
&lt;/h2&gt;

&lt;p&gt;The management of subscription lifecycles in Lemon Squeezy is handled through webhooks, which notify a specified URL upon subscription events—similar to Stripe’s webhook system. For a recent project, I used Cloudflare Workers to accept these webhooks and D1 to manage subscription data in a SQLite database. Opting for a serverless, lightweight architecture was important, especially since this is only handling a few API endpoints.&lt;/p&gt;

&lt;p&gt;My requirements for this included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A hosted checkout page ✅&lt;/li&gt;
&lt;li&gt;Use of a custom domain ✅&lt;/li&gt;
&lt;li&gt;Ability to exclusively sell products via in-app purchases, removing them from the main store ✅&lt;/li&gt;
&lt;li&gt;Capability to pass user metadata to the checkout page ✅&lt;/li&gt;
&lt;li&gt;Follow a similar subscription model to what I'm used to with Stripe ✅&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checkout lifecycle
&lt;/h2&gt;

&lt;p&gt;When users click the upgrade/checkout link within the app, they are redirected to a Lemon Squeezy hosted checkout page. Here, users input their details and payment information to create a subscription.&lt;/p&gt;

&lt;p&gt;The URL generated from within the app that points to the hosted checkout page includes necessary user details &lt;a href="https://docs.lemonsqueezy.com/help/checkout/passing-custom-data#passing-custom-data-in-checkout-links" rel="noopener noreferrer"&gt;passed as query parameters&lt;/a&gt;. For example, the URL might look like this (using template literals in JavaScript/TypeScript):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseCheckoutUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?checkout[email]=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&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="s2"&gt;&amp;amp;checkout[custom][uid]=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These parameters (&lt;code&gt;checkout[email]&lt;/code&gt; and &lt;code&gt;checkout[custom][uid]&lt;/code&gt;) pass the user's email and UID to the checkout page, linking them with the newly created subscription. The UID is used as a unique identifier for the user in the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; There's an edge case to consider: If a user can change their email address or navigates directly to the checkout page without a UID meta param, discrepancies might occur between the webhook payload and the user’s actual details. This issue should be considered when managing webhook responses and communicating with users in post-purchase scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Workers Routes
&lt;/h2&gt;

&lt;p&gt;When a subscription is created, Lemon Squeezy sends a webhook to the specified URL. In this setup, the URL is a Cloudflare Worker that receives requests at the &lt;code&gt;/api/v1/ls/webhook&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;To consolidate the handling of the site and API under the same domain, I use &lt;a href="https://developers.cloudflare.com/workers/configuration/routing/routes/" rel="noopener noreferrer"&gt;Workers Routes&lt;/a&gt; to route &lt;code&gt;/api/*&lt;/code&gt; to the Cloudflare Worker and &lt;code&gt;/*&lt;/code&gt; to the main site. This setup allows me to manage all of the site's operations under the same domain, managed by Cloudflare.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fklcvn20wqlex3rdujqns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fklcvn20wqlex3rdujqns.png" alt="Cloudflare Workers Routes" width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;small&gt;Cloudflare Workers Routes&lt;/small&gt;



&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

&lt;p&gt;This is a high-level overview of the Worker structure. In this app, this is the only API endpoint in &lt;code&gt;index.ts&lt;/code&gt;, but you can add more routes as needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
├─ db
│  └─ schema.ts
├─ lib
│  └─ utils.ts
├─ src
│  └─ index.ts
├─ wrangler.toml
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using Cloudflare D1
&lt;/h2&gt;

&lt;p&gt;In this example, the database schema is defined in the &lt;code&gt;schema.ts&lt;/code&gt; file and includes a &lt;code&gt;customer&lt;/code&gt; table with columns for the &lt;code&gt;uid&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;orderId&lt;/code&gt;, &lt;code&gt;productId&lt;/code&gt;, and &lt;code&gt;createdAt&lt;/code&gt; timestamp.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// db/schema.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drizzle-orm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sqliteTable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drizzle-orm/sqlite-core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sqliteTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer&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;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;primaryKey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;product_id&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;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created_at&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="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`CURRENT_TIMESTAMP`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The use of D1 is very compelling for this setup since the entire &lt;strong&gt;1/&lt;/strong&gt; API is located in Cloudflare, &lt;strong&gt;2/&lt;/strong&gt; it's SQL-based and works well with &lt;a href="https://orm.drizzle.team/" rel="noopener noreferrer"&gt;Drizzle ORM&lt;/a&gt;, and &lt;strong&gt;3/&lt;/strong&gt; the overhead to maintain is minimal. The &lt;code&gt;drizzle-orm&lt;/code&gt; package is used to interact with the database and execute SQL queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying Lemon Squeezy webhooks in a Cloudflare Worker
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;index.ts&lt;/code&gt; file contains the main logic for the Worker. The Worker listens for POST requests on the &lt;code&gt;/api/v1/ls/webhook&lt;/code&gt; endpoint and verifies the request signature using the secret provided by Lemon Squeezy. &lt;/p&gt;

&lt;p&gt;The Worker uses &lt;a href="https://hono.dev/" rel="noopener noreferrer"&gt;Hono&lt;/a&gt;, a lightweight framework for Cloudflare Workers, to handle requests. The framework provides middleware, routing, and other features to simplify the development of Workers. The Worker also uses Drizzle to interact with the D1 database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/index.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Hono&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hono&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hono/logger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;drizzle&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drizzle-orm/d1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./../db/schema&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;D1Database&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;LS_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Hono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Bindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/v1/ls/webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Method not allowed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LS_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;importKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&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;HMAC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; 
    &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verify&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nf"&gt;hexToUint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
    &lt;span class="nx"&gt;rawBody&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bodyJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bodyJson&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// handle additional events as needed&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventName&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;eventName&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription_created&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Event not supported&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// change/adjust as needed&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bodyJson&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;custom_data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bodyJson&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user_email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;identifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bodyJson&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bodyJson&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;first_order_item&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;guid&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;identifier&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;drizzle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;guid&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="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onConflictDoUpdate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To process the verification, Cloudflare Workers support the &lt;a href="https://developers.cloudflare.com/workers/runtime-apis/web-crypto/" rel="noopener noreferrer"&gt;Web Crypto API&lt;/a&gt; through the &lt;code&gt;SubtleCrypto&lt;/code&gt; interface, which is accessible via &lt;code&gt;crypto.subtle&lt;/code&gt;. The Worker first checks if the request method is POST and extracts the signature from the request headers. It then verifies the signature using the secret provided by Lemon Squeezy. If the signature is valid, the Worker continues processing the request.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;hexToUint8Array&lt;/code&gt; function is a utility function that converts a hex string to a &lt;code&gt;Uint8Array&lt;/code&gt;. &lt;code&gt;Uint8Array&lt;/code&gt; is used by the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Crypto/subtle" rel="noopener noreferrer"&gt;Web Crypto API&lt;/a&gt; for cryptographic operations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// utils.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hexToUint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/.&lt;/span&gt;&lt;span class="se"&gt;{1,2}&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matched&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;custom_data?.uid&lt;/code&gt; custom data is the user ID passed to the checkout page as a query parameter and is accessed in the &lt;a href="https://docs.lemonsqueezy.com/help/checkout/passing-custom-data#access-custom-data-in-webhooks" rel="noopener noreferrer"&gt;&lt;code&gt;meta&lt;/code&gt; object of the webhook payload&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Worker then parses the request body as JSON and extracts the event name, user email, order ID, and product ID from the request. The Worker checks if the event name is &lt;a href="https://docs.lemonsqueezy.com/guides/developer-guide/webhooks" rel="noopener noreferrer"&gt;&lt;code&gt;subscription_created&lt;/code&gt;&lt;/a&gt; and inserts the user details into the database using the Drizzle ORM. The Worker returns a &lt;code&gt;200&lt;/code&gt; status code if the request is successful.&lt;/p&gt;

&lt;p&gt;This code also assumes that the D1 database binding is configured in the &lt;code&gt;wrangler.toml&lt;/code&gt; file. The database configuration includes the database name, database ID, and migrations directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# wrangler.toml&lt;/span&gt;
&lt;span class="err"&gt;...&lt;/span&gt;

&lt;span class="nn"&gt;[[d1_databases]]&lt;/span&gt;
&lt;span class="py"&gt;binding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"DB"&lt;/span&gt;
&lt;span class="py"&gt;database_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;database-name&amp;gt;"&lt;/span&gt;
&lt;span class="py"&gt;database_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;database-id&amp;gt;"&lt;/span&gt;
&lt;span class="py"&gt;migrations_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"drizzle"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, the database configuration can be accessed as an environment variable in the Worker using &lt;code&gt;c.env.DB&lt;/code&gt; binding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the webhook signing secret
&lt;/h2&gt;

&lt;p&gt;For the Lemon Squeezy webhook request to be verified, the webhook secret signature needs to be stored as a Cloudflare Worker secret and then retrieved from the environment. &lt;/p&gt;

&lt;p&gt;I use the Wrangler CLI to create the secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler secret put LS_WEBHOOK_SECRET
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The secret can then be accessed in the Worker using &lt;code&gt;c.env.LS_WEBHOOK_SECRET&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The difference between secrets and environment variables is that secrets are encrypted and stored securely by Cloudflare. The environment variables are plaintext and can be accessed by anyone with access to the Worker code (config toml) or console access.&lt;/p&gt;




&lt;p&gt;This setup uses Cloudflare Workers and D1 to handle Lemon Squeezy subscription webhooks in a serverless architecture. You can easily extend this setup to handle additional events, API routes, and integrate with other services. It's straightforward to set up and maintain, and the lightweight architecture has been ideal so far.&lt;/p&gt;

&lt;p&gt;👋 If you have any questions or feedback, feel free to reach out on &lt;a href="https://x.com/siegerts" rel="noopener noreferrer"&gt;X @siegerts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Subscribe to get my latest content by email -&amp;gt; &lt;a href="https://siegerts.ck.page/74222e7e60" rel="noopener noreferrer"&gt;Newsletter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>lemonsqueezy</category>
      <category>cloudflare</category>
      <category>hono</category>
      <category>drizzle</category>
    </item>
    <item>
      <title>Detecting GitHub Issue Transfers in Chrome Extensions</title>
      <dc:creator>siegerts</dc:creator>
      <pubDate>Tue, 25 Jun 2024 15:33:55 +0000</pubDate>
      <link>https://forem.com/siegerts/detecting-github-issue-transfers-in-chrome-extensions-1pl8</link>
      <guid>https://forem.com/siegerts/detecting-github-issue-transfers-in-chrome-extensions-1pl8</guid>
      <description>&lt;p&gt;In GitHub issues, if an issue is transferred to another repository &lt;strong&gt;or&lt;/strong&gt; if the issue is converted to a discussion (or vice versa), the URL reference will change. Still it's not always easy to track these changes. Tracking transfers is pretty tricky, even using the GitHub API. If you're listening for webhook events on issues, it is possible to catch the &lt;code&gt;transferred&lt;/code&gt; event &lt;em&gt;but&lt;/em&gt; you &lt;em&gt;won't know where the issue was transferred to&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dossi.dev" rel="noopener noreferrer"&gt;dossi&lt;/a&gt;, the extension listens for these URL changes in the browser and stores the URL in the storage. When the URL changes, the extension checks if the URL is a redirect. If a redirect (transfer) is detected, the UI will notify the user.&lt;/p&gt;

&lt;p&gt;An example of a redirect is when an issue is transferred to another repository in GitHub. Or, if the organization or repository name changes, the URL will change.&lt;/p&gt;

&lt;p&gt;Here's a video of how this works:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/TdlyfXe1VXE"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;This is something that I wanted to account for after running into this issue when tracking issue transfers in the past. Mainly, these links get shared around and included in docs, Slack and Discord messages, and other places. If the issue is transferred, I didn't want to lose the reference to the issue. It's a nuanced edge case but it can cause some head-scratching when you're trying to track issues across repositories or organizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to track issue transfers (side quest)
&lt;/h2&gt;

&lt;p&gt;I've solved this in the past with the GitHub API but that requires &lt;a href="https://github.com/siegerts/contributor-metrics/tree/main" rel="noopener noreferrer"&gt;warehousing of all of the issues&lt;/a&gt; (typically from an organization) in order to catch the transfer event and then check for the existence of the issue in another repository. In short, you need to check for "duplicates" of the issue in other repositories. Here's an example of how I've queried for this in the past using the GitHub API and a warehouse of issue data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- SQL query to find duplicate issues in a database&lt;/span&gt;
&lt;span class="c1"&gt;-- username is the issue creator&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt;
            &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt;
        &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
            &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;
        &lt;span class="k"&gt;HAVING&lt;/span&gt;
            &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;small&gt;Reference: &lt;a href="https://github.com/siegerts/contributor-metrics/blob/93b703e0504f300e441f623b5a5ba3a3d62455f9/chalicelib/transfers.py#L35" rel="noopener noreferrer"&gt;&lt;strong&gt;contributor-metrics&lt;/strong&gt; GitHub repository&lt;/a&gt;&lt;br&gt;
&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;This will isolate issues that have been created at the same time by the same user - which is a good indicator that the issue has been transferred since the issue will appear in two different repositories. Then it's possible to ping the issues and check if the returned status is a &lt;code&gt;301&lt;/code&gt; with a new location header.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tracking issue transfers in dossi
&lt;/h2&gt;

&lt;p&gt;dossi uses the &lt;a href="https://developer.chrome.com/docs/extensions/reference/api/webNavigation" rel="noopener noreferrer"&gt;&lt;code&gt;chrome.webNavigation&lt;/code&gt;&lt;/a&gt; API to listen for URL changes in the browser and stores the URL in the storage. When the URL changes, the extension checks if the URL is a redirect. If it is, it stores the redirect URL in the storage. &lt;/p&gt;

&lt;p&gt;This is useful for tracking the URL changes in the browser and is available using the &lt;code&gt;chrome.webNavigation&lt;/code&gt; API. To use this API, you'll need to add the &lt;code&gt;webNavigation&lt;/code&gt; permission in the extension manifest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"webNavigation"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can add handlers to trigger for specific URL patterns using the &lt;code&gt;originAndPathMatches&lt;/code&gt; property in &lt;code&gt;chrome.webNavigation&lt;/code&gt;. In dossi, I'm using this to check for changes in discussions, issues, pull requests, and repositories in GitHub.&lt;/p&gt;

&lt;p&gt;Here's an example of how this is implemented in the background script of the extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// using the plasmohq/storage library&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Storage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@plasmohq/storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UrlMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Redirect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;patterns&lt;/span&gt; &lt;span class="o"&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;originAndPathMatches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`^https://github\.com/[a-zA-Z0-9\-_]+/[a-zA-Z0-9\-_]+/discussions/[0-9]+$`&lt;/span&gt;&lt;span class="p"&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;originAndPathMatches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`^https://github\.com/[a-zA-Z0-9\-_]+/[a-zA-Z0-9\-_]+/issues/[0-9]+$`&lt;/span&gt;&lt;span class="p"&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;originAndPathMatches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`^https://github\.com/[a-zA-Z0-9\-_]+/[a-zA-Z0-9\-_]+/pulls/[0-9]+$`&lt;/span&gt;&lt;span class="p"&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;originAndPathMatches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`^https://github\.com/[a-zA-Z0-9\-_]+/[a-zA-Z0-9\-_]+$`&lt;/span&gt;&lt;span class="p"&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;originAndPathMatches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`^https://github\.com/[a-zA-Z0-9\-_]+$`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="nx"&gt;patterns&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;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webNavigation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onBeforeNavigate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;from&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;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;UrlMatch&lt;/span&gt;&lt;span class="p"&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;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&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;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webNavigation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onCommitted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;redirect&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transitionQualifiers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server_redirect&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;logger&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server_redirect detected.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UrlMatch&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UrlMatch&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;from&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;UrlMatch&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;redirect&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;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Redirect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

          &lt;span class="c1"&gt;// remove from storage&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;from&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="p"&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;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;patterns&lt;/code&gt; array contains the URL patterns that the extension listens for.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;chrome.webNavigation.onBeforeNavigate&lt;/code&gt; event listener is triggered when the URL is about to change. The current URL is stored in the storage.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;chrome.webNavigation.onCommitted&lt;/code&gt; event listener is triggered when the URL has changed. If the URL has a &lt;code&gt;server_redirect&lt;/code&gt; transition qualifier, the extension checks if the URL has changed and if the URL is the same as the URL stored in the &lt;code&gt;onBeforeNavigate&lt;/code&gt; event listener. If the URL has changed but the pattern type is the same, the extension stores the redirect URL in the storage.&lt;/li&gt;
&lt;li&gt;The UI then displays a notification to the user in the side panel overlay.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;code&gt;server_redirect&lt;/code&gt; transition qualifier is one or more redirects caused by HTTP headers sent from the server happened during the navigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request lifecycle breakdown
&lt;/h2&gt;

&lt;p&gt;The pattern index (&lt;code&gt;pos&lt;/code&gt;) is used to map the URL pattern to the URL stored in the &lt;code&gt;onBeforeNavigate&lt;/code&gt; event listener. This is used to check if the changed URL type is the same as the type of the &lt;code&gt;onCommitted&lt;/code&gt; URL.&lt;/p&gt;

&lt;p&gt;So, an example of a GitHub issue transfer will follow this sequence of events:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;URL with a pattern for *GitHub issue fires in &lt;code&gt;onBeforeNavigate&lt;/code&gt; event listener&lt;/li&gt;
&lt;li&gt;URL with a pattern for GitHub issue fires in &lt;code&gt;onCommitted&lt;/code&gt; event listener&lt;/li&gt;
&lt;li&gt;URLs are not the same &lt;em&gt;but&lt;/em&gt; the positional mapping is the same&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;server_redirect&lt;/code&gt; is detected&lt;/li&gt;
&lt;li&gt;This indicates that the GitHub issue has been transferred&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;small&gt;*GitHub issue pattern: &lt;code&gt;^https://github\.com/[a-zA-Z0-9\-_]+/[a-zA-Z0-9\-_]+/issues/[0-9]+$&lt;/code&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.xiegerts.com%2Fpost%2Fdetecting-server-redirects-browser-extension%2Fimages%2Fdossi-issue-transfers.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.xiegerts.com%2Fpost%2Fdetecting-server-redirects-browser-extension%2Fimages%2Fdossi-issue-transfers.png" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;h2&gt;
  
  
  Surfacing the Redirects in the Extension
&lt;/h2&gt;

&lt;p&gt;The extension uses the &lt;code&gt;chrome.storage&lt;/code&gt; API to store the redirect URL. When the extension is opened, it checks if there is a redirect URL in the storage and displays a notification to the user in the side panel overlay.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu1r63aksulq0hzsdr2km.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu1r63aksulq0hzsdr2km.png" width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The user can choose to view the notes and transfer them to the correct entity (i.e. new issue/repo URL).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmxwz5k74hx9y1qflgrai.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmxwz5k74hx9y1qflgrai.png" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;If you only work across a few repositories then you may not hit this issue often. But if you're working across multiple repositories or organizations, this situation is likely to pop up. I wanted a way to capture these changes without having to query the GitHub API for every issue in every repository or track events via webhooks. This ended up being a nice in-between solution that works well client-side but also gives users the option to review the changes.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://dev.to/post/dossi-is-now-open-source/"&gt;dossi browser extension and the web app are both open source&lt;/a&gt;. Check them out on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/siegerts/dossi-ext" rel="noopener noreferrer"&gt;Browser extension&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/siegerts/dossi-app" rel="noopener noreferrer"&gt;Web app and API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The extension is available in the &lt;a href="https://chromewebstore.google.com/detail/ogpcmecajeghflaaaennkmknfpeghffm" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt;. If you have any questions or feedback, feel free to reach out on &lt;a href="https://x.com/siegerts" rel="noopener noreferrer"&gt;X @siegerts&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>chromeextension</category>
      <category>chromeapi</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Bring Your Own API Key: Supporting User-Provided OpenAI Keys and Prompts in Browser Extensions</title>
      <dc:creator>siegerts</dc:creator>
      <pubDate>Mon, 24 Jun 2024 05:34:21 +0000</pubDate>
      <link>https://forem.com/siegerts/bring-your-own-api-key-supporting-user-provided-openai-keys-and-prompts-in-browser-extensions-30md</link>
      <guid>https://forem.com/siegerts/bring-your-own-api-key-supporting-user-provided-openai-keys-and-prompts-in-browser-extensions-30md</guid>
      <description>&lt;p&gt;I recently added support (&lt;code&gt;v1.1.0+&lt;/code&gt;) for bringing your OpenAI API to &lt;a href="https://dossi.dev" rel="noopener noreferrer"&gt;dossi&lt;/a&gt; as an opt-in feature. This allows users to use their own OpenAI API key and prompts in the browser extension alongside parsed GitHub issue and discussion content. &lt;/p&gt;

&lt;p&gt;These are just a few thoughts on the implementation and some of my considerations when adding this in. &lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;It was important to me that the addition strikes a balance between allowing the generative AI functionality and not being too prescriptive in how it’s used.&lt;/p&gt;

&lt;p&gt;An example is in the video below. A user has enables the feature, adds their API key, and then adds a prompt. Then, the prompt is available to use when viewing a GitHub issue page and clicking the &lt;strong&gt;Prompts&lt;/strong&gt; button. The prompt messages are composed of the user's input (previous notes) and the parsed GitHub issue. Then, the prompt is sent to the OpenAI API and the response is displayed in the extension. This response can be edited and saved as a new note.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/q82eoIh1ecI"&gt;
&lt;/iframe&gt;
&lt;/p&gt;



&lt;p&gt;In dossi, the Gen AI prompt functionality is a nice-to-have that can enhance the experience for users already using OpenAI for other projects. Or, for users who already use the OpenAI API.&lt;/p&gt;

&lt;p&gt;Some common use cases for using prompts in dossi include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generating summaries of GitHub issues and discussions&lt;/li&gt;
&lt;li&gt;Creating reproducible steps for bug reports&lt;/li&gt;
&lt;li&gt;Monitoring the sentiment of a conversation&lt;/li&gt;
&lt;li&gt;Determining contributors to an issue or discussion&lt;/li&gt;
&lt;li&gt;Staying up to date on the latest comments and discussions in a repository&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To me, this can be a helpful tool for users who are managing multiple repositories, projects, or open-source organizations in GitHub. It can help them quickly get up to speed on the current state of an issue or discussion and help to reduce the overall time to close and triage for issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Considerations for adding Gen AI
&lt;/h2&gt;

&lt;p&gt;There are a few things to consider when adding this type of functionality. This isn't specific to dossi, but more of a general pattern that I've seen in other extensions that are backed by a hosted API and part of the reason why I decided to allow users to bring their own API key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; that this is more geared towards SaaS-type browser extensions.&lt;/p&gt;

&lt;p&gt;Extensions backed by hosted API that integrates with LLM model providers...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pricing model: Typically a flat rate subscription fee, usage (credit) based, or both. This complicates the tracking of usage and billing for the user and couples the extension to the API provider.&lt;/li&gt;
&lt;li&gt;Abuse: Since the extension is a client-side application, users can abuse the API. This could be in the form of excessive requests or malicious content. Even if the user is subscribed to dossi, they may try to refute the charges after using the LLM functionality.&lt;/li&gt;
&lt;li&gt;User-defined input: Accepting, processing, and &lt;em&gt;relying&lt;/em&gt; on arbitrary input (like GitHub issues) has a bunch of edge cases to account for (more on counting prompt input tokens below)&lt;/li&gt;
&lt;li&gt;Prompt response formatting: Everything works well until it doesn't. The response from the LLM model may not be what the user expects. This could be due to the prompt, the input, or the model itself.&lt;/li&gt;
&lt;li&gt;Handling errors without UX degradation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Allowing users to bring their own key eliminates most of the concerns. This also makes the functionality completely &lt;em&gt;opt-in&lt;/em&gt;. The UX of the extension won't be impacted if a user doesn't want to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Gen AI to dossi
&lt;/h2&gt;

&lt;p&gt;Here's the high-level overview of the implementation in dossi:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.xiegerts.com%2Fpost%2Fbrowser-extension-genai-key-prompts%2Fimages%2Fdossi-llm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.xiegerts.com%2Fpost%2Fbrowser-extension-genai-key-prompts%2Fimages%2Fdossi-llm.png" width="800" height="581"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Options Page&lt;/strong&gt; - Allow users to toggle on or off the use of their own API key. If enabled, a user can add their OpenAI API key and add in their own custom prompts. Once prompts exist, the UI button will be present when navigating GitHub issue and discussion pages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Local Storage&lt;/strong&gt; - In dossi, these settings are only saved on the local browser using the &lt;a href="https://developer.chrome.com/docs/extensions/reference/api/storage#storage_areas" rel="noopener noreferrer"&gt;&lt;code&gt;storage.local&lt;/code&gt;&lt;/a&gt;. These settings are never sent to our servers or persisted in a remote database. This is different from the note, pin, and label data that is persisted in a hosted database.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;Prompts&lt;/code&gt; button&lt;/strong&gt; - The button is only visible when a user is on a GitHub issue or discussion page. The button is only visible if the user has enabled the feature, added an API key, and added at least one prompt.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gen AI&lt;/strong&gt; - The prompt is constructed using the user's input and the parsed GitHub issue or discussion. The prompt is sent to the OpenAI API and the response is displayed in the extension. The user can edit the response and save it as a new note.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Error Handling&lt;/strong&gt; - Since this is the user's API key, I'm able to surface errors directly from the OpenAI SDK in the UI. Since the actions to configure the API key and prompts in dossi were explicit, the user has some awareness of the context of the error.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of the messaging passes through the background service worker of the extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating an Options Page
&lt;/h2&gt;

&lt;p&gt;I decided to add an &lt;a href="https://developer.chrome.com/docs/extensions/develop/ui/options-page" rel="noopener noreferrer"&gt;options page&lt;/a&gt; to the extension to make it easier and less cluttered to configure the local settings. Unlike the Chrome Side Panel API, an options page can be programmatically opened. This makes it convenient and more natural for users to navigate to. I added a settings link that triggers the page open in the extension user dropdown menus.&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;// background.ts&lt;/span&gt;

&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sendResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openOptionsPage&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;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openOptionsPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is referenced in the user menu that is shared across the extension &lt;strong&gt;popup&lt;/strong&gt; and &lt;strong&gt;content script&lt;/strong&gt; side panel overlay.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// user-account-nav.tsx&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DropdownMenuItem&lt;/span&gt;
   &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
     &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openOptionsPage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Settings
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;DropdownMenuItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding an API key and prompts
&lt;/h2&gt;

&lt;p&gt;This options page provides a way for users to opt-in to API key use. Currently, if &lt;strong&gt;opted-out&lt;/strong&gt;, then the side panel prompt button in the extension &lt;em&gt;will not be displayed&lt;/em&gt;. This makes the functionality completely opt-in and will not clutter the UI if a user isn't interested in using it.&lt;/p&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsdfadxlthr8z09thukrs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsdfadxlthr8z09thukrs.png" width="800" height="793"&gt;&lt;/a&gt;&lt;br&gt;
&lt;small&gt;Options page in dossi&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;If &lt;em&gt;enabled&lt;/em&gt;, a user can add their API key &lt;em&gt;and&lt;/em&gt; add in their own custom prompts. Once prompts exist, the UI button will be present when navigating GitHub issue and discussion pages. &lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/v0DEt2WxwLk"&gt;
&lt;/iframe&gt;
&lt;br&gt;
&lt;small&gt;Adding a prompt in dossi local settings&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Since dossi is used with GitHub, a valid prompt for issues and discussion can be pretty broad. A few factors quickly come to mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the user browsing, using, or maintaining the repo?&lt;/li&gt;
&lt;li&gt;What is the programming language used?&lt;/li&gt;
&lt;li&gt;Is the issue a bug, feature request, or question?&lt;/li&gt;
&lt;li&gt;Is subject matter expertise required?&lt;/li&gt;
&lt;li&gt;Is the /repo public or private?&lt;/li&gt;
&lt;li&gt;What is primary written language of the content?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd rather leave it up to the users to determine what information is the most useful for them.&lt;/p&gt;
&lt;h2&gt;
  
  
  Saving configuration locally with Chrome Storage
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
Always set spending limits on your API keys and monitor usage. The allowed permissions for the be as minimal as possible to reduce the risk of abuse.&lt;/p&gt;

&lt;p&gt;Persisting data locally with Chrome storage is always a bit tricky. In dossi, these settings are only local.&lt;/p&gt;

&lt;p&gt;This puts the control in the hands of the user. From a security perspective, there is no need for the extension or web app to store and manage user API keys on remote servers. If at any time a user wants to delete or invalidate their API key for use with dossi, they have at least 4 options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Delete the API key at the provider level (i.e. OpenAI)&lt;/li&gt;
&lt;li&gt;Clear all of their local dossi settings in the option page&lt;/li&gt;
&lt;li&gt;Overwrite their stored API key with a new or invalid one&lt;/li&gt;
&lt;li&gt;Remove the dossi extension (uninstall) which will wipe all of the associated storage data as per the Chrome API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Any of these actions can be done by the user, at any time. The note data, on the other hand, is remotely stored for a few reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The data also powers the web app and additional integrations for users (i.e. reporting)&lt;/li&gt;
&lt;li&gt;Storing large amounts of data locally may require additional browser extension permissions (&lt;code&gt;unlimitedData&lt;/code&gt; )&lt;/li&gt;
&lt;li&gt;Complex data models are difficult to troubleshoot, extend, rollback, and version - especially once published and in use&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Using the OpenAI JavaScript SDK
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/openai/openai-node" rel="noopener noreferrer"&gt;OpenAI JavaScript SDK&lt;/a&gt; is used to interact with the OpenAI API. To use the functionality, users are required to create an OpenAI API key and grant &lt;code&gt;write&lt;/code&gt; access for Model Capabilities and grant allowed models at the project level (i.e. &lt;code&gt;gpt-4o&lt;/code&gt; ).  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnty0qr1lunmcp1qz8yvm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnty0qr1lunmcp1qz8yvm.png" width="800" height="235"&gt;&lt;/a&gt;&lt;br&gt;
&lt;small&gt;OpenAI API key settings&lt;/small&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;p&gt;For the initial implementation, users can define the core prompt &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;maxTokens&lt;/code&gt;, and model (currently &lt;code&gt;gpt-4o&lt;/code&gt;). For reference, here is the prompt schema using &lt;a href="https://github.com/colinhacks/zod" rel="noopener noreferrer"&gt;&lt;code&gt;zod&lt;/code&gt;&lt;/a&gt;. Currently, the only available model is &lt;code&gt;gpt-4o&lt;/code&gt; and provider is OpenAI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is the initial shape of the prompt&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;promptSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Title must be at least 3 chars&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="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Title must be less than 25 chars&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content must be at least 50 chars&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="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content must be less than 1000 chars&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nonempty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refine&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid model&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;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Max tokens must be between 50 and 1000&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="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Max tokens must be between 50 and 1000&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These parameters are passed along with &lt;strong&gt;1)&lt;/strong&gt; the parsed page, and &lt;strong&gt;2)&lt;/strong&gt; any previous dossi notes for the page (i.e. dossi entity) to include as context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dangerouslyAllowBrowser&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="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// previous notes from user&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contextNotes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;
  &lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;promptInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  ---
  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
`&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatCompletionMessageParam&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&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;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You are a helpful technical assistant.&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="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;contextNotes&lt;/span&gt;
    &lt;span class="p"&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;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Past notes for context: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contextNotes&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ChatCompletionMessageParam&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="p"&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;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;promptInput&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I think in the future, including the previous user notes will be optional.  The &lt;code&gt;dangerouslyAllowBrowser&lt;/code&gt; flag is set to &lt;code&gt;true&lt;/code&gt; to allow the OpenAI SDK to be used in the browser. This is a requirement since the request is coming from the content script and not the background service worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;chatCompletion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatCompletion&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;chatCompletion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// surface error to user&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Not hiding the implementation
&lt;/h2&gt;

&lt;p&gt;By just providing the guardrails, I feel like it actually makes the use of the LLM functionality more tangible. The user shares control of the prompt input and output. Because of this, I feel like it gives the extension a bit more flexibility in what it does vs. the perceived "what it could do" of a completely hidden implementation. Also, the &lt;a href="https://github.com/siegerts/dossi-ext" rel="noopener noreferrer"&gt;extension is open source&lt;/a&gt;, so users can inspect the code and see how it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Surfacing errors
&lt;/h3&gt;

&lt;p&gt;The first instance is presenting the user with errors. Since this is the user's API key and they own the configuration of that key, I'm able to surface errors directly from the OpenAI SDK in the UI. Since the actions to configure the API key and prompts in dossi are explicit, the user has some awareness of the context of the error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fieq005u5r6rq73baysln.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fieq005u5r6rq73baysln.png" width="800" height="302"&gt;&lt;/a&gt;&lt;br&gt;
&lt;small&gt;Surfaced OpenAI API error in dossi&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;I'm assuming that the user has some familiarity with the OpenAI API and can troubleshoot the error. If the error is related to the prompt, the user can edit the prompt and try again. If the error is related to the API key, the user can check the key and try again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Editing the prompt response
&lt;/h3&gt;

&lt;p&gt;Secondly, the user can edit the prompt response directly in the extension and save it as a new note. This is a nice way to allow the user to review and update the response and make it more useful for their needs. Also, it's a nice way to smooth over any errors that may have occurred in the initial response.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzy3qynlfpqefyeznck2e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzy3qynlfpqefyeznck2e.png" width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Flexibility on edge cases
&lt;/h2&gt;

&lt;p&gt;There are a lot of edge cases when manually constructing prompts and including arbitrary (and unknown) input context in the form of parsed GitHub issues. The first is model token limitations. Models have constraints on the amount of input tokens, output tokens, and sometimes a combination. One approach to account for this is to count the prompt tokens before sending them to the model to gauge whether the prompt will meet the criteria. &lt;em&gt;To be honest, this isn't on my radar to add&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is a challenge in dossi since a GitHub issue may be extremely long with historical data. Additionally, bundling the dependencies into the extension may cause the size to increase impacting the overall experience for a user. &lt;/p&gt;

&lt;p&gt;I've decided to allow these types of errors to surface to the user instead of trying to intelligently truncate the context which could change the overall intent. Also, since this is the user's API key, I don't want to trigger additional calls to the completion APIs (i.e. chunk and summarize) without their explicit actions since there will be associated costs.&lt;/p&gt;

&lt;p&gt;I may add a feature in the future to allow the user to manually truncate the input context if they are running into token limitations. Or, at a minimum, allow the parsed GitHub content to be viewed in the extension before sending the prompt to the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving forward
&lt;/h2&gt;

&lt;p&gt;Hope that you found this interesting! This is just the first iteration of the generative AI functionality in dossi. I'm excited to see how it's used.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://dev.to/post/dossi-is-now-open-source/"&gt;browser extension and the web app are both open source&lt;/a&gt;. Check them out on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/siegerts/dossi-ext" rel="noopener noreferrer"&gt;Browser extension&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/siegerts/dossi-app" rel="noopener noreferrer"&gt;Web app and API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The extension is available in the &lt;a href="https://chromewebstore.google.com/detail/ogpcmecajeghflaaaennkmknfpeghffm" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;If you have any questions or feedback, feel free to reach out on &lt;a href="https://x.com/siegerts" rel="noopener noreferrer"&gt;X @siegerts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Subscribe to get my latest content by email -&amp;gt; &lt;a href="https://siegerts.ck.page/74222e7e60" rel="noopener noreferrer"&gt;Newsletter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>genai</category>
      <category>chromeextension</category>
      <category>opensource</category>
    </item>
    <item>
      <title>dossi is now open source</title>
      <dc:creator>siegerts</dc:creator>
      <pubDate>Sun, 05 May 2024 00:33:55 +0000</pubDate>
      <link>https://forem.com/siegerts/dossi-is-now-open-source-54ke</link>
      <guid>https://forem.com/siegerts/dossi-is-now-open-source-54ke</guid>
      <description>&lt;h2&gt;
  
  
  Backstory
&lt;/h2&gt;

&lt;p&gt;I built dossi as a way to keep track of my own notes for GitHub issues and pull requests across multiple repositories and organizations.&lt;/p&gt;

&lt;p&gt;Personally, a challenge that I kept running into was just saving general thoughts at the moment when I found a new open-source project or issue that I knew I'd want to come back to later. Initially, this was to help with issue triage and reproducibility.&lt;/p&gt;

&lt;p&gt;For organizations that I was a part of, I wanted something more private than just having the GitHub visibility set to private (just for myself). I felt that the existing tools were too focused on teams building and collaborating on code, and not enough on the experience of those that are also supporting customers and users of the software. Later, I found that I was able to track increases in interest (reactions) on issues and keep track of the general sentiment around a request or issue.&lt;/p&gt;

&lt;p&gt;Yes, there are GitHub notifications and stars, but that quickly becomes overwhelming especially when you're working across an organization or multiple organizations with a lot of repositories, in addition to your own personal projects.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/wgGGjAqa3L8"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sidepanel&lt;/strong&gt; overlay on GitHub pages to add, edit, and delete notes

&lt;ul&gt;
&lt;li&gt;Label creation&lt;/li&gt;
&lt;li&gt;Pin pages to view later&lt;/li&gt;
&lt;li&gt;Note management (create, read, update, delete, sort) with "light" markdown support&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Extension &lt;strong&gt;Popup&lt;/strong&gt; window to view recent activity and pins&lt;/li&gt;

&lt;li&gt;Content script button to open the sidepanel overlay and display the number of notes for the current page&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Transferred page detection&lt;/strong&gt; to prompt the user to transfer notes to the new page entity&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Web app&lt;/strong&gt; to view all notes across all repositories and organizations&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Development
&lt;/h2&gt;

&lt;p&gt;The project (obviously) started as a closed source project. I wasn't 100% sure which direction to take it and, quite honestly, it just started as a fun project to build. This was also my first time building a browser extension so there were a lot of unknowns on how it would shape up.&lt;/p&gt;

&lt;p&gt;Browser extensions definitely have their own set of challenges, especially when it comes to the UI and UX. I used the &lt;a href="https://github.com/PlasmoHQ/plasmo" rel="noopener noreferrer"&gt;Plasmo&lt;/a&gt; extension framework to get started quickly, iterate fast, and not have to worry about the boilerplate code that comes with building a browser extension especially when it comes to the Manifest v3 configuration.&lt;/p&gt;

&lt;p&gt;Most of the time spent was the parity between the extension and the web app with the UI look and feel. Extension content script styling has some challenges since you can have collisions with the host page's CSS. dossi uses &lt;a href="https://ui.shadcn.com/" rel="noopener noreferrer"&gt;shadcn/ui&lt;/a&gt; for the UI components which makes it easy to keep the UI look and feel consistent across the extension and the web app - but uses Tailwind for the styling. GitHub &lt;em&gt;also uses Tailwind&lt;/em&gt; so there are challenges that require some overrides to make sure the UI components look consistent across the extension and the web app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separating the extension and web app
&lt;/h3&gt;

&lt;p&gt;The first decision is the separation of functionality between the browser extension and the web app.&lt;/p&gt;

&lt;p&gt;Thinking back, I could have forgone the web app and just used the browser extension as the main interface. That may be something to consider in the future and would simplify the architecture quite a bit. I wanted to have a meta-view of all of the notes across all of the repositories and organizations that I was keeping track of - and filter and search across them.&lt;/p&gt;

&lt;p&gt;That said, there is some nice flexibility in having the API separate from the extension. &lt;/p&gt;

&lt;p&gt;A few things come to mind...&lt;/p&gt;

&lt;h4&gt;
  
  
  Storage schema management
&lt;/h4&gt;

&lt;p&gt;Changes to the storage schema can be managed in the API (and API versioning) and not have to worry about the extension &lt;em&gt;and the extension review process&lt;/em&gt; that can take a few days to publish an updated version&lt;/p&gt;

&lt;h4&gt;
  
  
  Chrome storage API
&lt;/h4&gt;

&lt;p&gt;Using the &lt;a href="https://developer.chrome.com/docs/extensions/reference/api/storage" rel="noopener noreferrer"&gt;chrome.storage&lt;/a&gt; API is flexible but backward compatibility can be a challenge. Typically, the first suggestion is to use a storage library or database but troubleshooting client-side migrations for users usually requires an uninstall/reinstall of the extension. This is not ideal for a user who has a lot of notes saved since they will lose all of their notes. Users will likely just stop using the extension if they have to do this.&lt;/p&gt;

&lt;h4&gt;
  
  
  Data model complexity
&lt;/h4&gt;

&lt;p&gt;The mental model of the Storage API can get complex quickly when the extension interacts with it in multiple ways across background scripts, content scripts, and popup scripts.&lt;/p&gt;

&lt;h4&gt;
  
  
  Meta datatable view
&lt;/h4&gt;

&lt;p&gt;The web app can be used as a backup for notes and also as a way to view notes across multiple devices. This is something that I thought could be a potential use case in the future.&lt;/p&gt;

&lt;h4&gt;
  
  
  Allow notes to be used outside of the extension
&lt;/h4&gt;

&lt;p&gt;It felt limiting to restrict the notes to just the extension. I wanted to be able to use the notes in other applications and services. This is something that I haven't fully explored yet but I think it could be a powerful feature to have.&lt;/p&gt;

&lt;h4&gt;
  
  
  Integration with other services
&lt;/h4&gt;

&lt;p&gt;I've given thought to integrating with other services like Slack, Discord, and email to send notifications and reminders about notes that are saved. And, also the ability to share notes with other users (or organizations) that are also using dossi.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auth state management
&lt;/h3&gt;

&lt;p&gt;The extension and web app use &lt;a href="https://next-auth.js.org/" rel="noopener noreferrer"&gt;NextAuth.js&lt;/a&gt; for authentication. This was a decision to reduce the friction of auth state between the extension and the web app. The extension uses the NextAuth.js client to authenticate and uses the host permissions of the extension to use the session cookie to authenticate with the API. The host permissions are set to the API of the web app. This allows the extension to use the session cookie to authenticate with the API and not have to worry about the Chrome storage API to manage the auth state. Also, if a user is already authenticated with the web app, they will be automatically authenticated with the extension. This is a nice feature to have since the extension can be used across multiple devices and the auth state is managed in one place.&lt;/p&gt;

&lt;p&gt;That said, although users sign in with their GitHub account, &lt;em&gt;the GitHub API is not used for data access or retrieval&lt;/em&gt;. This was a decision to keep the notes private and not have to worry about the GitHub API rate limits. Also, I was contemplating expanding the notes to other services and didn't want to be tied to the GitHub API.&lt;/p&gt;

&lt;p&gt;The interaction between the extension and the web app is shown in the diagram below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2iawl3kyxlkv458u93rs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2iawl3kyxlkv458u93rs.png" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tech stack
&lt;/h3&gt;

&lt;p&gt;The tech stack was largely driven by the use of Next.js, NextAuth.js, &lt;a href="https://www.prisma.io/" rel="noopener noreferrer"&gt;Prisma&lt;/a&gt;, and shadcn/ui. I've recently migrated the database to a serverless Postgres database using &lt;a href="https://neon.tech/" rel="noopener noreferrer"&gt;Neon Postgres&lt;/a&gt; from MySQL (more on this in a future post). Locally, I build and test using a development database branch using Neon branches. I'd like to lean more into Postgres features and extensions in the future.&lt;/p&gt;

&lt;p&gt;Here's a breakdown of what's used in different parts of the project:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Browser extension&lt;/th&gt;
&lt;th&gt;Web app&lt;/th&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plasmo&lt;/td&gt;
&lt;td&gt;browser extension framework&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;language&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shadcn/ui&lt;/td&gt;
&lt;td&gt;UI components&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;CSS&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://react-query.tanstack.com/" rel="noopener noreferrer"&gt;TanStack/react-query&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;data fetching&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/colinhacks/zod" rel="noopener noreferrer"&gt;zod&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;schema validation&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;td&gt;framework&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Neon Postgres&lt;/td&gt;
&lt;td&gt;Serverless Postgres database&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prisma&lt;/td&gt;
&lt;td&gt;database ORM and client&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NextAuth.js&lt;/td&gt;
&lt;td&gt;auth&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://stripe.com/" rel="noopener noreferrer"&gt;Stripe&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;payments&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;deployments and hosting&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As and aside, the use of TanStack/react-query was a game changer for me. I was able to remove a lot of boilerplate code for syncing data in the extension. I was able to consolidate the data fetching and caching logic. &lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment
&lt;/h3&gt;

&lt;p&gt;The API is hosted on Vercel and the extension is hosted on the Chrome Web Store and Firefox Add-ons. The extension submission process differs between the Chrome Web Store and Firefox Add-ons. &lt;/p&gt;

&lt;p&gt;For the extension, I haven't automated the end-to-end testing and deployment process. I like to spot the builds and versioning before uploading the artifacts to the web stores. &lt;/p&gt;

&lt;p&gt;The web app and API are integrated with the GitHub repos and automatically deploy on push to the main branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future plans
&lt;/h2&gt;

&lt;p&gt;Overall, I'm excited to see what the community does with the project. It seemed a bit daunting to transition the project to open source but I'm excited to see how the project can be used in other ways that I haven't thought of yet.&lt;/p&gt;

&lt;p&gt;I've already received some feedback and feature requests that I'm excited to work on, so stay tuned for updates.&lt;/p&gt;

&lt;p&gt;If you have any feedback or feature requests, feel free to open an issue on the GitHub repositories below or reach out to me on Twitter/X &lt;a href="https://x.com/siegerts" rel="noopener noreferrer"&gt;@siegerts&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/siegerts/dossi-ext" rel="noopener noreferrer"&gt;Browser extension&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/siegerts/dossi-app" rel="noopener noreferrer"&gt;Web app and API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The extension is available in the &lt;a href="https://chromewebstore.google.com/detail/ogpcmecajeghflaaaennkmknfpeghffm" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have any questions or feedback, feel free to reach out on &lt;a href="https://x.com/siegerts" rel="noopener noreferrer"&gt;X @siegerts&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>github</category>
      <category>notes</category>
      <category>chromeextension</category>
    </item>
    <item>
      <title>Move Fast and “Branch” Things</title>
      <dc:creator>siegerts</dc:creator>
      <pubDate>Mon, 15 Apr 2024 16:38:10 +0000</pubDate>
      <link>https://forem.com/siegerts/move-fast-and-branch-things-4con</link>
      <guid>https://forem.com/siegerts/move-fast-and-branch-things-4con</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2MIhHrQC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://neondatabase.wpengine.com/wp-content/uploads/2024/04/image-28-1024x576.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2MIhHrQC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://neondatabase.wpengine.com/wp-content/uploads/2024/04/image-28-1024x576.png" alt="serverless postgres" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Neon simplifies the use of &lt;a href="https://neon.tech/blog/neon-ga" rel="noopener noreferrer"&gt;scalable Postgres&lt;/a&gt;, changing how you handle your database infrastructure from development through production. This is a fundamental shift in how to leverage your Postgres infrastructure. Instead of spending time on Infrastructure as code (IaC) and cloud-specific configuration tasks in services like Amazon RDS or Google Cloud SQL, with Neon you can increase developer velocity and focus on building for your customer.&lt;/p&gt;

&lt;p&gt;When making architecture decisions for any app, it is always important to consider the constraints of your system and under what circumstances those constraints and requirements apply, both during periods of low request traffic and during high-traffic bursts. Neon maintains Postgres compatibility while enhancing the developer experience with database branching, autoscaling, and serverless scale to zero – enabling &lt;em&gt;you&lt;/em&gt; to launch and scale your projects quickly.&lt;/p&gt;

&lt;p&gt;Neon is the most productive way for builders to use Postgres.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimizing Total Cost of Ownership (TCO) in Database Infrastructure
&lt;/h2&gt;

&lt;p&gt;A traditional vendor database setup requires a complex architecture involving virtual private clouds (VPCs), subnet configurations, and manual replication configurations across availability zones. Another aspect of this setup is managing access, typically done through a bastion host or a jump box, which serves as a gateway for updates or connections to the data source. &lt;/p&gt;

&lt;p&gt;Implementing features like High Availability (HA) and autoscaling in these environments further complicates the architecture. For example, this is a subset of considerations to take into account when provisioning an RDS instance in AWS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complex Setup and Maintenance&lt;/strong&gt; : Configuring an instance requires attention to numerous settings, making the setup and ongoing maintenance a complex task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling Challenges&lt;/strong&gt; : Properly scaling an instance to match fluctuating applications requires understanding of workload patterns, optimized instance types, and auto-scaling policies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High Availability&lt;/strong&gt; : Ensuring continuous availability requires additional setup and planning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost Management&lt;/strong&gt; : Deciding instance types, storage, and backup options to balance performance and total expense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Management and Networking&lt;/strong&gt; : Correctly configuring VPCs, security groups, and Identity and Access Management (IAM) roles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backup and Recovery&lt;/strong&gt; : Configuring and managing backups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, these architectures lack the advantages of a truly serverless architecture that can scale to zero, increasing cost, complexity, and resource demands. This complexity results in a significant investment of time, both initially and for ongoing maintenance, introducing a steep learning curve. &lt;/p&gt;

&lt;p&gt;All of this is needed &lt;em&gt;before&lt;/em&gt; even using the database.&lt;/p&gt;

&lt;p&gt;These factors combined contribute to a higher total cost of ownership, especially when considering how such an architecture integrates with the broader ecosystem of your application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Want scale to zero serverless?
&lt;/h3&gt;

&lt;p&gt;Neon is &lt;a href="https://neon.tech/blog/why-you-want-a-database-that-scales-to-zero" rel="noopener noreferrer"&gt;scale-to-zero Postgres&lt;/a&gt;. No changes are needed. Your compute scales down (to idle) when not in use and scales up in capacity to handle increased requests and manage capacity. This also results in cost savings for production-like environments. Your environments can be exact copies of each other in &lt;code&gt;dev&lt;/code&gt;/&lt;code&gt;test&lt;/code&gt;/&lt;code&gt;prod&lt;/code&gt; but &lt;em&gt;not incur additional costs&lt;/em&gt; when not in use or receiving traffic. Additional services, proxies, or cloud event logging aren’t needed. Enough said.&lt;/p&gt;

&lt;p&gt;True serverless architecture can significantly transform the way a managed Postgres platform is utilized. This transition can improve developer productivity by allowing them to concentrate on creating value, while also enhancing the scalability and availability of applications. This leads to quicker development cycles and increased reliability of the applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Need connection pooling?
&lt;/h3&gt;

&lt;p&gt;Without Neon that is a separate configuration, piece of infrastructure to manage, and service fee. With Neon, it’s included. &lt;a href="https://neon.tech/blog/pgbouncer-the-one-with-prepared-statements#what-is-pgbouncer" rel="noopener noreferrer"&gt;Neon supports connection pooling using PgBouncer&lt;/a&gt;, which allows your database on Neon to support up to 10,000 concurrent connections. &lt;/p&gt;

&lt;h3&gt;
  
  
  Want your database to autoscale?
&lt;/h3&gt;

&lt;p&gt;Neon dynamically adjusts the amount of compute resources allocated to a Neon compute endpoint in response to the current load, eliminating the need for manual intervention. This is autoscaling-on-the-fly. &lt;/p&gt;

&lt;p&gt;Without Neon, you’re faced with either managing the autoscaling yourself by monitoring and adjusting instance types and policies with RDS, or settling for &lt;a href="https://neon.tech/blog/aurora-serverless-v1-to-neon" rel="noopener noreferrer"&gt;increased cold starts and lack of scale to zero with Aurora&lt;/a&gt; (20-60s on Aurora V1). Even with Aurora V2 and RDS, you’ll likely need to use connection pooling (above). This means you might have to set up Amazon RDS Proxy or run PgBouncer on a separate Amazon Elastic Compute Cloud (Amazon EC2) instance to manage your database connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move fast and “branch” things
&lt;/h2&gt;

&lt;p&gt;Neon’s database branching enables you to clone databases in &lt;strong&gt;~1 second&lt;/strong&gt;. Until you’ve experienced Neon Postgres Branching – you don’t know what you’re missing. A branch is a &lt;a href="https://neon.tech/docs/reference/glossary#copy-on-write" rel="noopener noreferrer"&gt;copy-on-write&lt;/a&gt; clone of your data, making it possible to perform operations on branches independently without impacting the original data. This is a different way to think about the database. Instead of just a piece of infrastructure – Neon is like Git for Postgres. With that in mind, you start to envision different ways to use the database to accelerate your workflow, not just ways to manage the database. &lt;/p&gt;

&lt;p&gt;Interacting with your Postgres database in a similar way to using git unlocks workflows for teams, feature dev, and test environments have for a long time been an operational burden – and has been a task reserved for DevOps teams.&lt;/p&gt;

&lt;p&gt;It’s possible to create a Postgres database branch with &lt;strong&gt;one&lt;/strong&gt; CLI command:&lt;/p&gt;



&lt;p&gt;&lt;em&gt;This is not possible with other Postgres vendors&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;In seconds, you can branch your entire database as shown in the video below:&lt;/p&gt;



&lt;p&gt;Neon’s &lt;a href="https://neon.tech/blog/architecture-decisions-in-neon" rel="noopener noreferrer"&gt;separation of storage and compute&lt;/a&gt; makes this possible – allowing for flexible and independent scaling of each. For comparison, to isolate development/test/production database environments in a traditional cloud environment, each of these environments would require the provisioning of a separate database cluster or database within a cluster for isolation. It’s not possible to branch your Postgres database in this manner with other vendors.&lt;/p&gt;

&lt;p&gt;To isolate a new Postgres database in Amazon RDS or Aurora Postgres, the only option is to create a new instance or replicate an existing one. This task, while ultimately not providing the equivalent outcome, is significantly more complex and time-consuming. Creating a new RDS Postgres instance takes &lt;strong&gt;~5 minutes&lt;/strong&gt;. This also does not account for the role and policy IAM updates, new infra, and additional storage costs for the copy.&lt;/p&gt;

&lt;p&gt;Neon branching empowers developers to take a more iterative and experimental development approach, allowing for rapid prototyping and testing without risk to the production environment, accelerating development feedback loops and innovation. &lt;/p&gt;

&lt;p&gt;Branching is easy, cost-effective, and fast. &lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/j4Vak4J10KU"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Neon branches create an entire copy of your Postgres Cluster. Branches can then be used to isolate tests, analyses, and new feature launches without compromising the integrity of the main branch. Because of this, it’s even easier to perform data recovery or roll back to a previous version of the database. By restoring a specific branch, developers can recover data efficiently, a task that is more time-consuming with traditional vendor solutions. This adds an extra layer of security and flexibility, enabling teams to manage their data with greater agility and minimizes the potential for data loss.&lt;/p&gt;

&lt;p&gt;Neon branches collapse the timeline from hours, days, or weeks into seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer Workflow Integration
&lt;/h2&gt;

&lt;p&gt;With just a connection string, Neon enables developers to take full advantage of Postgres. This puts the database in the hands of the developer without sacrificing the scalability, reliability, and performance of a production-grade developer experience. Your existing Postgres workflow and toolset works with Neon since Neon is Postgres. This includes Integrated Development Environments (IDEs) and framework integrations like Django, Rails, Node.js, etc. These will all work without the use of cloud vendor-specific SDKs (and dependencies). &lt;/p&gt;

&lt;p&gt;Additionally, developers can manage and connect to Neon using open-source tools: the &lt;a href="https://neon.tech/docs/reference/neon-cli" rel="noopener noreferrer"&gt;Neon CLI&lt;/a&gt; for direct command-line interaction, the &lt;a href="https://neon.tech/docs/reference/api-reference" rel="noopener noreferrer"&gt;Neon API&lt;/a&gt; for programmatic access and integration, and the &lt;a href="https://github.com/neondatabase/serverless" rel="noopener noreferrer"&gt;Neon Serverless Driver&lt;/a&gt;, which works within serverless architectures and runtimes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Neon Command Line Interface (CLI)
&lt;/h3&gt;

&lt;p&gt;The integration of the Neon CLI with development tools and CI/CD pipelines enhances developer workflows, reducing the friction associated with database-related operations like creating projects, new databases, and branches. Once you have your connection string, you can manage your entire Neon database. This makes it possible to quickly set up &lt;a href="https://neon.tech/docs/guides/branching-github-actions" rel="noopener noreferrer"&gt;deployment pipelines such as GitHub Actions,&lt;/a&gt; GitLab CI/CD, or Vercel Preview Environments. These operations and pipelines can also be treated as code and live alongside your applications as they evolve and mature.&lt;/p&gt;

&lt;p&gt;This integration translates to quicker deployments, easier rollback processes, and a more robust deployment experience, all contributing to increased developer velocity. Or, these operations can be made using the Neon API across different languages and frameworks.&lt;/p&gt;

&lt;p&gt;Below is a video to demonstrate how to restore a Neon Postgres database in just a few seconds:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/ZnxLCOkb_R0"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect from Serverless Environments
&lt;/h3&gt;

&lt;p&gt;The Neon Serverless Driver for JavaScript and TypeScript is a &lt;a href="https://neon.tech/blog/http-vs-websockets-for-postgres-queries-at-the-edge" rel="noopener noreferrer"&gt;low-latency Postgres driver for JavaScript and TypeScript&lt;/a&gt; that allows you to query data from serverless and edge environments over HTTP or WebSockets where direct access to TCP is restricted.&lt;/p&gt;

&lt;p&gt;This can be used in &lt;a href="https://neon.tech/blog/serverless-api-using-aws-lambda-cdk-and-neon" rel="noopener noreferrer"&gt;serverless environments like AWS Lambda&lt;/a&gt;, Cloudflare Workers, and Vercel Edge functions. With a few lines of code, you can connect to and query your database the same way that you would in a backend persistent server instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;neon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@neondatabase/serverless&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;neon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;);&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;post&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="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`SELECT * FROM posts WHERE id = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the programming model consistent – developers can use Neon the same way that they would normally use Postgres but with an enhanced and flexible experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres as a Developer Tool
&lt;/h2&gt;

&lt;p&gt;With the movement away from Postgres as just a piece of infrastructure, developers can (and should) view Neon Postgres as an instrumental tool to enhance the scalability and functionality of their applications and workloads. Two examples are in Generative AI and Software as a Service (SaaS) applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leveraging AI in your application
&lt;/h3&gt;

&lt;p&gt;Use Neon Postgres with the &lt;a href="https://neon.tech/blog/pgvector-30x-faster-index-build-for-your-vector-embeddings" rel="noopener noreferrer"&gt;pgvector extension&lt;/a&gt; as the way to store vector embeddings and enhance your models using Retrieval Augmented Generation (RAG) with vector search.&lt;/p&gt;

&lt;p&gt;Neon can scale up on-demand to build your index and scale back down to save on cost. This on-demand scaling can lead to cost savings compared to traditional, always-on database instances that are overprovisioned to accommodate peak usage periods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building SaaS platforms
&lt;/h3&gt;

&lt;p&gt;The streamlined experience of using Neon to use Postgres makes it even more possible to build Software as a service (SaaS) platforms that have requirements to &lt;a href="https://neon.tech/blog/how-retool-uses-retool-and-the-neon-api-to-manage-300k-postgres-databases" rel="noopener noreferrer"&gt;rapidly provision Postgres&lt;/a&gt; and even isolate those instances across tenants using database branching. With traditional cloud vendors, this is costly and operationally intense – in terms of provisioning time, maintenance, and cost. &lt;/p&gt;

&lt;p&gt;Neon helps to unblock these use cases and empower SaaS builders to take full advantage of Postgres on a per customer or tenant basis. Effectively allowing SaaS businesses to provide their Platform as a Service (PaaS).&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Neon abstracts away the complexities traditionally associated with database management—such as infrastructure setup, connection pooling, and autoscaling—Neon allows developers and builders to concentrate on what they do best: building innovative and scalable web applications…fast.&lt;/p&gt;

&lt;p&gt;To get started with Serverless Postgres, sign up and &lt;a href="https://console.neon.tech/signup" rel="noopener noreferrer"&gt;try Neon for free&lt;/a&gt;. Follow us on &lt;a href="https://twitter.com/neondatabase" rel="noopener noreferrer"&gt;Twitter/X&lt;/a&gt;, join us on &lt;a href="https://neon.tech/discord" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;, and let us know how we can help you build the next generation of applications.&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>fullstack</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Fullstack Serverless CI/CD in AWS Amplify Hosting with Postgres Database Branching</title>
      <dc:creator>siegerts</dc:creator>
      <pubDate>Wed, 06 Mar 2024 23:50:49 +0000</pubDate>
      <link>https://forem.com/neon-postgres/fullstack-serverless-cicd-in-aws-amplify-hosting-with-postgres-database-branching-2j7</link>
      <guid>https://forem.com/neon-postgres/fullstack-serverless-cicd-in-aws-amplify-hosting-with-postgres-database-branching-2j7</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZR8gqHFY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://neondatabase.wpengine.com/wp-content/uploads/2024/03/neon-amplify-1024x576.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZR8gqHFY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://neondatabase.wpengine.com/wp-content/uploads/2024/03/neon-amplify-1024x576.jpg" alt="Neon Postgres with AWS Amplify Hosting" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post will guide you through integrating &lt;a href="https://aws.amazon.com/amplify/hosting/" rel="noopener noreferrer"&gt;AWS Amplify Hosting&lt;/a&gt; CI/CD with Neon Postgres with a focus on testing your application’s environment or feature branches using real data enabled by Neon’s &lt;a href="https://neon.tech/docs/introduction/branching" rel="noopener noreferrer"&gt;database branching&lt;/a&gt;. This architecture automates your workflow and ensures each new feature or environment branch in your Amplify application has a corresponding, isolated database. This is especially useful if you’re building and deploying a Server Side Rendering (SSR) application using frameworks like &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt;, &lt;a href="https://nuxt.com/" rel="noopener noreferrer"&gt;Nuxt&lt;/a&gt;, &lt;a href="https://astro.build/" rel="noopener noreferrer"&gt;Astro&lt;/a&gt;, and &lt;a href="https://kit.svelte.dev/" rel="noopener noreferrer"&gt;SvelteKit&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Because both Neon and Amplify Hosting are serverless, the underlying infrastructure doesn’t require maintenance. Also, when not in use, both services scale down to zero. For Amplify Hosting, this involves serverless compute functions (i.e SSR), while for Neon, it pertains to the Postgres cluster compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution Overview
&lt;/h2&gt;

&lt;p&gt;In this guide, we cover the deployment process of a serverless full-stack app using Amplify Hosting, leveraging its SSR compute capabilities. SSR enables running code and logic in a server environment, useful for tasks like using routing middleware, interfacing with third-party services, or connecting to a database.&lt;/p&gt;

&lt;p&gt;To follow along, check out the YouTube video below that walks through in depth the process of deploying, and verifying the database branch creation in Neon when deploying a Nuxt SSR app that uses an &lt;a href="https://docs.amplify.aws/gen2/" rel="noopener noreferrer"&gt;Amplify Gen 2 backend&lt;/a&gt; with Amplify Auth. While the example features Nuxt for server-side APIs, the methodology applies to any &lt;a href="https://docs.aws.amazon.com/amplify/latest/userguide/amplify-ssr-framework-support.html" rel="noopener noreferrer"&gt;SSR framework compatible with Amplify Hosting’s function-based compute&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For practical implementation, the scripts referenced in this guide (and video) are located in the &lt;a href="https://github.com/siegerts/neon-branches-amplify-cicd" rel="noopener noreferrer"&gt;Neon Branches with Amplify Hosting CI/CD&lt;/a&gt; repo on GitHub.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/sBRI4QAZ-oo"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Amplify Hosting
&lt;/h2&gt;

&lt;p&gt;AWS Amplify Hosting is a fully managed CI/CD and hosting service that supports static sites, single-page applications (SPAs), and full-stack serverless SSR apps.&lt;/p&gt;

&lt;p&gt;In Amplify Hosting, a typical workflow involves &lt;a href="https://docs.aws.amazon.com/amplify/latest/userguide/multi-environments.html" rel="noopener noreferrer"&gt;teams managing app branches that align with various environments&lt;/a&gt;, such as development, testing, and production. Each branch, like dev, test, and prod, corresponds to its respective environment and is accessible via a unique URL, which incorporates the first-level subdomain and the branch identifier. For example, &lt;code&gt;https://&amp;lt;branch-name&amp;gt;.&amp;lt;amplify-app-id&amp;gt;.amplifyapp.com/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serverless Postgres
&lt;/h2&gt;

&lt;p&gt;When developing an SSR app in AWS Amplify Hosting and requiring Postgres or an RDBMS, the choices for a serverless relational database are limited. It’s important that your database can handle your application traffic without manual adjustment since serverless SSR fullstack apps can scale out in response to bursts of request traffic. And then scale back down when not in use – similar to how Lambda (and cloud) functions operate.&lt;/p&gt;

&lt;p&gt;Using Amazon RDS involves virtual private cloud (VPC) and architecture configurations, typically requiring a Lambda function or service proxy for database access to bridge the connection from Amplify Hosting to the VPC. Alternatively, directly exposing an RDS database online requires setting up Amazon RDS Proxy, essentially acting as &lt;a href="https://neon.tech/blog/pgbouncer-the-one-with-prepared-statements" rel="noopener noreferrer"&gt;PgBouncer&lt;/a&gt;, for scaling purposes. &lt;/p&gt;

&lt;p&gt;Amazon DynamoDB (DDB) is another serverless database solution, but it doesn’t offer the relational capabilities inherent to RDBMS like Postgres, such as relational data models, complex joins, or transactions.  &lt;/p&gt;

&lt;p&gt;Neon is serverless Postgres with &lt;a href="https://neon.tech/blog/scaling-serverless-postgres" rel="noopener noreferrer"&gt;auto-scaling&lt;/a&gt;, easy setup, &lt;a href="https://neon.tech/docs/connect/connection-pooling" rel="noopener noreferrer"&gt;built-in connection pooling&lt;/a&gt;, and database branching. Neon’s branching capability allows developers to clone their entire Postgres cluster in seconds, creating isolated environments for testing new features without affecting the primary database branch. For these reasons, it pairs well with the serverless nature of Amplify Hosting and frontend fullstack SSR application architectures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Amplify Hosting CI/CD Integration
&lt;/h2&gt;

&lt;p&gt;This Amplify Hosting SSR app and Neon Postgres integration involves a custom Bash script (&lt;code&gt;neon-ci.sh&lt;/code&gt;) &lt;a href="https://github.com/siegerts/neon-branches-amplify-cicd/blob/main/neon-ci.sh" rel="noopener noreferrer"&gt;to manage Neon database branches alongside Amplify app branches&lt;/a&gt;. This script dynamically updates the .env file’s &lt;code&gt;DATABASE_URL&lt;/code&gt; environment variable with the database connection string for each branch. This &lt;code&gt;.env&lt;/code&gt; file is then loaded into the application’s server-side runtime as environment variables. To interact with the Postgres database in serverless or edge contexts, the Neon &lt;a href="https://neon.tech/blog/serverless-driver-for-postgres" rel="noopener noreferrer"&gt;Serverless Driver for JavaScript and TypeScript&lt;/a&gt; can be used. Additionally, database migrations can be integrated into the CI/CD pipeline since the connection string is available in the build time environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1YFRn2ZS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://neondatabase.wpengine.com/wp-content/uploads/2024/03/diagram-2-1024x512.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1YFRn2ZS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://neondatabase.wpengine.com/wp-content/uploads/2024/03/diagram-2-1024x512.png" alt="CI/CD flow with Neon Postgres and AWS Amplify Hosting" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Here’s how the integration works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;A new Git branch is created and pushed in a repo that is configured for fullstack continuous deployments in Amplify Hosting&lt;/li&gt;
&lt;li&gt;During the CI/CD process, a new Neon Postgres branch will be created using the Neon CLI via a custom bash script&lt;/li&gt;
&lt;li&gt;This branch’s connection string is injected into the &lt;code&gt;DATABASE_URL&lt;/code&gt; parameter within a &lt;code&gt;.env&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;The application is deployed and has access to the &lt;code&gt;DATABASE_URL&lt;/code&gt; environment variable in server-side compute functions&lt;/li&gt;
&lt;li&gt;Connect to your database using the Neon serverless driver&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  In order to set up this flow, you’ll need to complete the following steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Set up your Amplify app and CI/CD pipeline as per your project requirements&lt;/li&gt;
&lt;li&gt;Choose the &lt;strong&gt;Amazon Linux: 2023&lt;/strong&gt; build image and enable automatic service role creation (if you are not using another role)&lt;/li&gt;
&lt;li&gt;Copy the &lt;a href="https://github.com/siegerts/neon-branches-amplify-cicd" rel="noopener noreferrer"&gt;neon-ci.sh script and amplify.yml&lt;/a&gt; build settings into the root of your app directory &lt;/li&gt;
&lt;li&gt;Enable branch auto-detection for the app to create a new Amplify app branch for each new Git branch matching the configured pattern&lt;/li&gt;
&lt;li&gt;Create a &lt;a href="https://neon.tech/github" rel="noopener noreferrer"&gt;Neon project and create an API key&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Create an &lt;a href="https://neon.tech/blog/deploy-a-serverless-fastapi-app-with-neon-postgres-and-aws-app-runner-at-any-scale#create-a-databaseurl-parameter-in-ssm-parameter-store" rel="noopener noreferrer"&gt;SSM &lt;strong&gt;SecureString&lt;/strong&gt; parameter&lt;/a&gt; for the Neon API key&lt;/li&gt;
&lt;li&gt;Add the correct policy permissions to the Amplify app Service role for SSM Parameter Store and Amplify from the AWS CLI within the CI/CD build time&lt;/li&gt;
&lt;li&gt;Modify &lt;code&gt;neon-ci.sh&lt;/code&gt; invocation in your &lt;code&gt;amplify.yml&lt;/code&gt; build settings with your specific Neon project ID, database name, and other parameters as needed&lt;/li&gt;
&lt;li&gt;Push your changes and monitor the Amplify console for the deployment status – the Neon console will show the created branch(es)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Configuring Amplify Hosting Build Settings
&lt;/h3&gt;

&lt;p&gt;To set up build settings in Amplify Hosting, first connect your code repository to AWS Amplify Hosting to activate the CI/CD pipeline. Amplify supports GitHub, Bitbucket, GitLab, or AWS CodeCommit as source control providers. &lt;/p&gt;

&lt;h4&gt;
  
  
  Create an Amplify app
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qSEB7CSd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://neondatabase.wpengine.com/wp-content/uploads/2024/03/creat-amplify-app-1024x433.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qSEB7CSd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://neondatabase.wpengine.com/wp-content/uploads/2024/03/creat-amplify-app-1024x433.png" alt="Create an Amplify Hosting Gen 2 app" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Confirm the app settings
&lt;/h4&gt;

&lt;p&gt;Choose the build image for your application (i.e. &lt;strong&gt;Amazon Linux: 2023)&lt;/strong&gt; and enable automatic service role creation (if you are not using another role). This role will be updated to allow access to your Neon API key parameter in SSM Parameter Store.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qX6nFgYn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://neondatabase.wpengine.com/wp-content/uploads/2024/03/amplify-app-build-settings-1024x353.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qX6nFgYn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://neondatabase.wpengine.com/wp-content/uploads/2024/03/amplify-app-build-settings-1024x353.png" alt="Amplify app build settings" width="800" height="276"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Update the Amplify Hosting Build Settings
&lt;/h3&gt;

&lt;p&gt;The app build settings are configured in the Amplify Hosting console &lt;em&gt;or&lt;/em&gt; checked in as &lt;code&gt;amplify.yml&lt;/code&gt;within your code repository.&lt;/p&gt;

&lt;p&gt;This &lt;code&gt;amplify.yml&lt;/code&gt; file defines the CI/CD pipeline configuration for AWS Amplify Hosting. It is split into backend and frontend stages, with specific commands executed at different phases ( &lt;strong&gt;preBuild&lt;/strong&gt; , &lt;strong&gt;build&lt;/strong&gt; , and &lt;strong&gt;postBuild&lt;/strong&gt; ) for both the backend and frontend stages.&lt;/p&gt;

&lt;p&gt;Customize the build process by &lt;a href="https://github.com/siegerts/neon-branches-amplify-cicd/blob/main/amplify.yml" rel="noopener noreferrer"&gt;adjusting the amplify.yml file&lt;/a&gt;, which controls the actions taken during the CI/CD pipeline stages for both frontend and backend components.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;phases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;preBuild&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sudo yum -y install jq&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;jq --version&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm i -g neonctl@v1&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nvm use &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;corepack enable&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pnpm install&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;if ["${AWS_BRANCH}" = "main"] || ["${AWS_BRANCH}" = "dev"]; then &lt;/span&gt;
            &lt;span class="s"&gt;pnpm amplify pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;pnpm amplify generate config --branch main --app-id $AWS_APP_ID &lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

          &lt;span class="s"&gt;bash neon-ci.sh create-branch --app-id $AWS_APP_ID --neon-project-id &amp;amp;lt;neon-project-id&amp;amp;gt; --branch-name $AWS_BRANCH --parent-branch main --api-key-param "&amp;amp;lt;ssm-param&amp;amp;gt;" --role-name &amp;amp;lt;neon-role&amp;amp;gt; --database-name &amp;amp;lt;neon-db-name&amp;amp;gt; --suspend-timeout 0&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$(pnpm store path)&lt;/span&gt;
&lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;phases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;preBuild&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nvm use &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;corepack enable&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npx --yes nypm i&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;

  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;baseDirectory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.amplify-hosting&lt;/span&gt;
    &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adjust as needed depending on your app framework requirements, and also make sure to update the &lt;code&gt;neon-ci.sh&lt;/code&gt; input arguments. The following is a step-by-step breakdown of the sample &lt;code&gt;amplify.yml&lt;/code&gt;set up:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pre-Build Phase:

&lt;ol&gt;
&lt;li&gt;Installs &lt;a href="https://jqlang.github.io/jq/" rel="noopener noreferrer"&gt;jq for JSON processing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Installs the &lt;code&gt;neonctl&lt;/code&gt; CLI tool globally using npm&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;Build Phase:

&lt;ol&gt;
&lt;li&gt;Sets the Node version using nvm and enables corepack for package manager version management&lt;/li&gt;
&lt;li&gt;Based on the branch (main or dev), it deploys the backend Amplify app and then invokes the &lt;code&gt;neon-ci.sh&lt;/code&gt; script to manage Neon database branches&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sets up Node, installs dependencies, and executes the build process for the frontend&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Cache Configuration:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Caches the &lt;a href="https://pnpm.io/" rel="noopener noreferrer"&gt;pnpm&lt;/a&gt; store path to enhance the speed of future builds&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Creating a new Postgres Database in Neon
&lt;/h3&gt;

&lt;p&gt;Visit the Neon Console, sign up, and create your first project by following the prompts in the UI. Then, &lt;a href="https://neon.tech/docs/manage/api-keys#create-an-api-key" rel="noopener noreferrer"&gt;create an API key&lt;/a&gt; for your Neon account. This key should be stored in SSM Parameter Store and accessed by your Amplify build scripts for authentication with Neon for database branching. Whether you’re working with an existing project or starting a new one, this project will host the database linked to your Amplify SSR app. When you create a new branch in the Amplify app, a corresponding database branch is automatically created in your specified Neon project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--plW9QIzH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://neondatabase.wpengine.com/wp-content/uploads/2024/02/get-started-with-neon-1024x789.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--plW9QIzH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://neondatabase.wpengine.com/wp-content/uploads/2024/02/get-started-with-neon-1024x789.png" alt="Get started with Neon Postgres" width="800" height="616"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Add the Neon CI bash script
&lt;/h3&gt;

&lt;p&gt;Include the &lt;a href="https://github.com/siegerts/neon-branches-amplify-cicd/blob/main/neon-ci.sh" rel="noopener noreferrer"&gt;Neon bash CI script&lt;/a&gt; in your application directory. This will be referenced during the backend build stage of the CI/CD pipeline. This script can be modified, if needed, to account for your specific use case but the primary function is to create a branch for each Amplify app branch and retrieve the connection string.&lt;/p&gt;

&lt;p&gt;The script has the following commands and options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/neon-ci.sh &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt;&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;options]

Commands
create-branch:
    Creates a new Neon database branch.

options:
    &lt;span class="nt"&gt;--app-id&lt;/span&gt; &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt;app-id&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;--neon-project-id&lt;/span&gt; &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt;project-id&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;--parent-branch-id&lt;/span&gt; &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt;parent-branch-id&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;--api-key-param&lt;/span&gt; &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt;api-key-param&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;--role-name&lt;/span&gt; &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt;role-name&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;--database-name&lt;/span&gt; &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt;database-name&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;--suspend-timeout&lt;/span&gt; &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt;suspend-timeout&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;

cleanup-branches:
    Cleans up Neon database branches that no longer
    have corresponding Amplify app branches.

options:
    &lt;span class="nt"&gt;--app-id&lt;/span&gt; &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt;app-id&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;--neon-project-id&lt;/span&gt; &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt;project-id&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;--api-key-param&lt;/span&gt; &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt;api-key-param&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These commands, specifically &lt;code&gt;create-branch&lt;/code&gt;, are used during the backend &lt;strong&gt;build&lt;/strong&gt; phase of the CI/CD process. &lt;/p&gt;

&lt;h3&gt;
  
  
  Update the Amplify Hosting app backend build role with the correct policy statements
&lt;/h3&gt;

&lt;p&gt;Update the Amplify Hosting backend build role with the policies below to allow access to SSM Parameter Store and Amplify using the AWS CLI within the CI/CD build environment. The &lt;code&gt;neon-ci&lt;/code&gt; bash script calls out to SSM and Amplify which will require the service role to have the correct permissions to access.&lt;/p&gt;

&lt;p&gt;An example of this from the bash script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# neon-ci.sh&lt;/span&gt;
...

&lt;span class="k"&gt;function &lt;/span&gt;set_neon_api_key &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;NEON_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ssm get-parameter &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$NEON_API_KEY_PARAM&lt;/span&gt; &lt;span class="nt"&gt;--with-decryption&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; Parameter.Value &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEON_API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: NEON_API_KEY is not set. Exiting."&lt;/span&gt;
        &lt;span class="nb"&gt;exit &lt;/span&gt;1
    &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To update the role, add the below inline policies to the Amplify app backend build role in AWS Identity and Access Management (IAM). Scope these resources as required by your app.&lt;/p&gt;

&lt;h3&gt;
  
  
  For SSM Parameter Store
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowAmplifySSMCalls"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssm:GetParametersByPath"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssm:GetParameters"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssm:GetParameter"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ssm:*:*:parameter/&amp;amp;lt;name-of-your-parameter&amp;amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  For AWS Amplify access
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Statement1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"amplify:ListBranches"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:amplify:*:*:apps/*/branches/*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable branch auto-detection for the app to create a new Amplify app branch
&lt;/h3&gt;

&lt;p&gt;Update the applications repository settings to &lt;a href="https://docs.aws.amazon.com/amplify/latest/userguide/pattern-based-feature-branch-deployments.html" rel="noopener noreferrer"&gt;enable branch auto-detection&lt;/a&gt;. Once enabled, a new app branch will automatically create new Amplify app branches when a new Git branch is created and pushed to the repo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--145NwaZN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://neondatabase.wpengine.com/wp-content/uploads/2024/03/branch-auto-detection-1024x402.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--145NwaZN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://neondatabase.wpengine.com/wp-content/uploads/2024/03/branch-auto-detection-1024x402.png" alt="Branch auto detection in Amplify Hosting" width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Post build database branch clean up
&lt;/h2&gt;

&lt;p&gt;If you want to automate database branch cleanup after each build, then you can include logic to use the clean up branches function in the Neon CI bash script. The &lt;code&gt;cleanup-branches&lt;/code&gt; function will pull all of your Amplify Hosting app branch references with the aws cli and then compare those with the branch names in your Neon project.&lt;/p&gt;

&lt;p&gt;_ &lt;strong&gt;Note:&lt;/strong&gt; Make sure to test any destructive branch actions on test apps and branches first before deploying to production CI/CD pipelines._&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;postBuild&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="c1"&gt;# EXAMPLE: only run the cleanup-branches command if you have tested &lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;if ! ["${AWS_BRANCH}" = "main"] &amp;amp;amp;&amp;amp;amp; ! ["${AWS_BRANCH}" = "dev"]; then&lt;/span&gt;
              &lt;span class="s"&gt;# bash neon-ci.sh cleanup-branches --app-id $AWS_APP_ID --neon-project-id &amp;amp;lt;neon-project-id&amp;amp;gt; --api-key-param "&amp;amp;lt;ssm-param&amp;amp;gt;"&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;

&lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Connecting to Postgres with Nuxt SSR Server Routes
&lt;/h2&gt;

&lt;p&gt;Now, you can use the Neon Serverless Driver to connect to your database. For example, using Nuxt SSR in Amplify Hosting with &lt;a href="https://nuxt.com/docs/guide/directory-structure/server#server-routes" rel="noopener noreferrer"&gt;server routes&lt;/a&gt;, you can query your database branch using a similar pattern as below:&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;// ./server/routes/api/shows.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;neon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@neondatabase/serverless&lt;/span&gt;&lt;span class="dl"&gt;"&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;databaseUrl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRuntimeConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;neon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;databaseUrl&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`SELECT * FROM netflix_shows limit 10`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;shows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The environment variable is populated into Nuxt with &lt;code&gt;useRuntimeConfig()&lt;/code&gt;.&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;// nuxt.config.js&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;devtools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&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="na"&gt;runtimeConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The private keys which are only available within server-side&lt;/span&gt;
    &lt;span class="na"&gt;databaseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// Keys within public, will be also exposed to the client-side&lt;/span&gt;
    &lt;span class="na"&gt;public&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;//&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Practices and Considerations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Securely manage your Neon API key. Do not commit this key to git. Use a SSM Parameter Store SecureString to store and access your Neon API key securely within your build environment.&lt;/li&gt;
&lt;li&gt;Implement logging within your build scripts to track the creation and deletion of database branches. This will be useful for troubleshooting and auditing your CI/CD process.&lt;/li&gt;
&lt;li&gt;Integrate database migration scripts into your CI/CD pipeline to update the schema of your database branches automatically when necessary. &lt;a href="https://orm.drizzle.team/" rel="noopener noreferrer"&gt;Drizzle&lt;/a&gt; and &lt;a href="https://www.prisma.io/" rel="noopener noreferrer"&gt;Prisma&lt;/a&gt; can be integrated into your build process for this using the database branch connection string environment variable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Integrating Neon database branches with Amplify Hosting CI/CD provides a flexible way to automate environment isolation and database creation – in a truly serverless way.&lt;/p&gt;

&lt;p&gt;To get started with incorporating Serverless Postgres into your Amplify Hosting SSR apps, &lt;a href="https://console.neon.tech/signup" rel="noopener noreferrer"&gt;sign up and try Neon for free&lt;/a&gt;. Follow us on &lt;a href="https://twitter.com/neondatabase" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; and join us in &lt;a href="https://neon.tech/discord" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; to share your experiences, suggestions, and challenges.&lt;/p&gt;

</description>
      <category>awsamplify</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Deploy a Serverless FastAPI App with Neon Postgres and AWS App Runner at any scale</title>
      <dc:creator>siegerts</dc:creator>
      <pubDate>Fri, 09 Feb 2024 17:16:46 +0000</pubDate>
      <link>https://forem.com/neon-postgres/deploy-a-serverless-fastapi-app-with-neon-postgres-and-aws-app-runner-at-any-scale-1lnm</link>
      <guid>https://forem.com/neon-postgres/deploy-a-serverless-fastapi-app-with-neon-postgres-and-aws-app-runner-at-any-scale-1lnm</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fba5kpcv7ywai0y1yhwg7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fba5kpcv7ywai0y1yhwg7.jpg" alt="Neon Postgres with FastAPI" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this post, we’ll guide you through setting up a scalable serverless API using &lt;a href="https://fastapi.tiangolo.com/" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt;, deployed on &lt;a href="https://docs.aws.amazon.com/apprunner/latest/dg/what-is-apprunner.html" rel="noopener noreferrer"&gt;AWS App Runner&lt;/a&gt; and powered by Neon Postgres as the serverless database.&lt;/p&gt;

&lt;p&gt;FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.8+ based on standard Python type hints. The key features of FastAPI are its speed and ease of use, making it an excellent choice for building robust APIs. FastAPI has quickly become a go-to framework for setting up Python APIs and services.&lt;/p&gt;

&lt;p&gt;AWS App Runner is a fully managed service that makes it easy for developers to quickly deploy containerized web applications and APIs, at scale. These services will automatically scale the instances up or down for your App Runner application in accordance to incoming traffic volume.&lt;/p&gt;

&lt;p&gt;Neon complements this setup by providing a serverless Postgres database that &lt;a href="https://neon.tech/docs/introduction/autoscaling" rel="noopener noreferrer"&gt;scales compute resources automatically&lt;/a&gt;, optimizing performance based on demand.&lt;/p&gt;

&lt;p&gt;We’ll walk through deploying a FastAPI application with Neon Serverless Postgres, focusing on secure database connection management via AWS Systems Manager (SSM) Parameter Store. This approach allows for flexible application environment management across development, testing, and production stages.&lt;/p&gt;

&lt;p&gt;Let’s get started!&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;To follow along and deploy the application in this guide, you will need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An AWS account, with access to AWS App Runner for deploying and managing your application&lt;/li&gt;
&lt;li&gt;A GitHub or BitBucket account, for linking to AWS App Runner and enabling CI/CD functionality&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://console.neon.tech/signup" rel="noopener noreferrer"&gt;A Neon account&lt;/a&gt; – The FastAPI app will connect to a Neon serverless Postgres database 🚀&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We’ll start by setting up a local development environment and getting our application running. Then, we’ll connect the application to a Neon Postgres database to ensure it can scale efficiently. Finally, we’ll set up automatic deployment through AWS App Runner, which will deploy our application upon each commit to a Git repository.  &lt;/p&gt;

&lt;p&gt;This infrastructure provides a flexible architecture that will scale automatically as needed by the API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up FastAPI with Poetry
&lt;/h3&gt;

&lt;p&gt;In this app, we’ll use &lt;a href="https://python-poetry.org/" rel="noopener noreferrer"&gt;poetry&lt;/a&gt; to manage the dependencies in the local Python virtual environment. For reference, the Python version used to write this post is 3.11. This version will also match the &lt;a href="https://docs.aws.amazon.com/apprunner/latest/dg/service-source-code-python-releases.html" rel="noopener noreferrer"&gt;App Runner Python 3.11 runtime&lt;/a&gt; during deployment. &lt;/p&gt;

&lt;p&gt;To start, use poetry to create a new project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;poetry new fastapi-neon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create a project structure that we’ll use to build out the FastAPI app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;fastapi-neon&lt;/span&gt;
&lt;span class="s"&gt;├── pyproject.toml&lt;/span&gt;
&lt;span class="s"&gt;├── README.md&lt;/span&gt;
&lt;span class="s"&gt;├── fastapi-neon&lt;/span&gt;
&lt;span class="s"&gt;│ └── __init__.py&lt;/span&gt;
&lt;span class="s"&gt;└── tests&lt;/span&gt;
    &lt;span class="s"&gt;└── __init__.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, initialize a new &lt;em&gt;git&lt;/em&gt; repository within the project directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the app is working locally, you’ll push the repo to GitHub and then deploy to AWS App Runner using the built-in CI/CD workflow.&lt;/p&gt;

&lt;h4&gt;
  
  
  Installing FastAPI and uvicorn
&lt;/h4&gt;

&lt;p&gt;To isolate project dependencies, poetry will create a Python virtual environment associated with the project. To serve the application, we’ll use &lt;a href="https://fastapi.tiangolo.com/deployment/manually/#run-a-server-manually-uvicorn" rel="noopener noreferrer"&gt;uvicorn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, using poetry (or your preferred dependency manager), install &lt;code&gt;fastapi&lt;/code&gt; and &lt;code&gt;uvicorn&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;poetry add fastapi &lt;span class="s2"&gt;"uvicorn[standard]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, add a _ &lt;strong&gt;main.py&lt;/strong&gt; _ app into the &lt;strong&gt;fastapi_neon&lt;/strong&gt; app directory and serve the app:&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="c1"&gt;# fastapi_neon/main.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_root&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;World&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/items/{item_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;item_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serve the app locally with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;poetry run uvicorn fastapi_neon.main:app &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Awesome! The app will be functional on your local machine. Check the Swagger UI endpoint by visiting &lt;code&gt;http://0.0.0.0:8000/docs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv0z0ljgzqt9p4ts2jcjx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv0z0ljgzqt9p4ts2jcjx.png" alt="fastapi-swagger" width="800" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating the application to connect to Neon
&lt;/h2&gt;

&lt;p&gt;Now that we have an app working locally, we’ll connect the application code to a Neon Postgres database.&lt;/p&gt;

&lt;p&gt;For configuration management, we’ll use the &lt;a href="https://www.starlette.io/config/" rel="noopener noreferrer"&gt;Starlette configuration pattern&lt;/a&gt;. This will make it possible to reference predefined environment variables. FastAPI is based on Starlette, so we’ll be able to use this functionality without adding an additional dependency.&lt;/p&gt;

&lt;p&gt;To connect to a Postgres database from the app, we’ll add an &lt;a href="https://sqlmodel.tiangolo.com/" rel="noopener noreferrer"&gt;SQLModel&lt;/a&gt; (based on SQLAlchemy). SQLModel is a library for interacting with SQL databases from Python code, with Python objects. &lt;/p&gt;

&lt;p&gt;Since SQLModel is driver agnostic, we’ll need to install a Postgres driver to enable it to connect to Neon. For this, we’ll use &lt;a href="https://www.psycopg.org/psycopg3/docs/index.html" rel="noopener noreferrer"&gt;psycopg 3&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Add the &lt;code&gt;sqlmodel&lt;/code&gt; and &lt;code&gt;psycopg&lt;/code&gt; dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;poetry add sqlmodel &lt;span class="s2"&gt;"psycopg[binary]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: The default driver behavior for SQLAlchemy is to look for/use psycopg2. If you plan on using psycopg2 then you’ll need to install &lt;code&gt;psycopg2-binary&lt;/code&gt; and adjust the connection string formatting in the &lt;code&gt;create_engine&lt;/code&gt; code below.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now, replace the existing &lt;code&gt;main.py&lt;/code&gt; application code with the app code below:&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="c1"&gt;# fastapi_neon/main.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asynccontextmanager&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi_neon&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlmodel&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SQLModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;select&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SQLModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# only needed for psycopg 3 - replace postgresql
# with postgresql+psycopg in settings.DATABASE_URL
&lt;/span&gt;&lt;span class="n"&gt;connection_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql+psycopg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# recycle connections after 5 minutes
# to correspond with the compute scale down
&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;connection_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connect_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sslmode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;require&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;pool_recycle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_db_and_tables&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;SQLModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# The first part of the function, before the yield, will
# be executed before the application starts
&lt;/span&gt;&lt;span class="nd"&gt;@asynccontextmanager&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lifespan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Creating tables..&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;create_db_and_tables&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lifespan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lifespan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_root&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;World&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/todos/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_todo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/todos/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_todos&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;todos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example API allows clients to create (POST) and retrieve (GET) todos. The database connection string is imported from settings and used to instantiate the connection in &lt;code&gt;create_engine&lt;/code&gt;. All of the values are cast as &lt;code&gt;Secret&lt;/code&gt; to limit their exposure in logs in the event that they are exposed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fastapi.tiangolo.com/advanced/events/" rel="noopener noreferrer"&gt;Lifespan events&lt;/a&gt; are the recommended way to execute code once the server starts in FastAPI. In this example, the &lt;code&gt;Todo&lt;/code&gt; model (i.e. table) is created if it doesn’t yet exist in the database. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;pool_recycle=300&lt;/code&gt; option is an “optimistic” approach to &lt;a href="https://docs.sqlalchemy.org/en/20/core/pooling.html#setting-pool-recycle" rel="noopener noreferrer"&gt;prevent the pool from using a connection that has passed a certain age&lt;/a&gt;. In this case, we are setting the value to 5 minutes to correspond with the default &lt;a href="https://neon.tech/docs/guides/auto-suspend-guide" rel="noopener noreferrer"&gt;compute auto-suspend in Neon&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Another option to account for possible stale connections is to use the &lt;code&gt;pool_pre_ping&lt;/code&gt; option. This option is used to test the availability of a database connection before the connection is used. Note, this can add additional latency to new connections since they are first “checked”.&lt;/p&gt;

&lt;p&gt;Next, we’ll get the connection string for the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrate Neon Postgres with FastAPI
&lt;/h3&gt;

&lt;p&gt;Create a &lt;strong&gt;&lt;em&gt;settings.py&lt;/em&gt;&lt;/strong&gt; file in the &lt;strong&gt;&lt;em&gt;fastapi_neon&lt;/em&gt;&lt;/strong&gt; directory alongside &lt;strong&gt;&lt;em&gt;main.py&lt;/em&gt;&lt;/strong&gt;. This will reference the &lt;code&gt;DATABASE_URL&lt;/code&gt; environment variable. During local development, this value will be populated from an _ &lt;strong&gt;.env&lt;/strong&gt; _ file.&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="c1"&gt;# fastapi_neon/settings.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.datastructures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Secret&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.env&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cast&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;strong&gt;&lt;em&gt;.env&lt;/em&gt;&lt;/strong&gt; file in the root of your project and add the &lt;code&gt;DATABASE_URL&lt;/code&gt; variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .env&lt;/span&gt;
&lt;span class="c1"&gt;# Don't commit this to source control&lt;/span&gt;
&lt;span class="c1"&gt;# Eg. Include &amp;amp;quot;.env&amp;amp;quot; in your `.gitignore` file.&lt;/span&gt;
&lt;span class="s"&gt;DATABASE_URL=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note: Do not commit the &lt;em&gt;.env&lt;/em&gt; file to source control. Add &lt;em&gt;.env&lt;/em&gt; to the &lt;em&gt;.gitignore&lt;/em&gt; file to prevent this file from being committed and pushed. This environment variable in this file will only be used during local development.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Visit the &lt;a href="https://console.neon.tech/" rel="noopener noreferrer"&gt;Neon Console&lt;/a&gt;, sign up, and create your first project by following the prompts in the UI. You can use an existing project or create another if you’ve already used Neon.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fneondatabase.wpengine.com%2Fwp-content%2Fuploads%2F2024%2F02%2Fget-started-with-neon-1024x789.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fneondatabase.wpengine.com%2Fwp-content%2Fuploads%2F2024%2F02%2Fget-started-with-neon-1024x789.png" alt="Get started with Neon for free&amp;lt;br&amp;gt;
" width="800" height="616"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Visit the project Dashboard, check the &lt;strong&gt;Pooled connection&lt;/strong&gt; option, and select the &lt;strong&gt;Connection string&lt;/strong&gt; option from the &lt;strong&gt;Connection Details&lt;/strong&gt; panel.&lt;/p&gt;

&lt;p&gt;The connection string will be in the following format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;postgres://alex:AbC123dEf@ep-cool-darkness-123456-pooler.us-east-2.aws.neon.tech/dbname?sslmode&lt;span class="o"&gt;=&lt;/span&gt;require
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set this value as your &lt;code&gt;DATABASE_URL&lt;/code&gt; in the &lt;strong&gt;&lt;em&gt;.env&lt;/em&gt;&lt;/strong&gt; file. Now, test the application by running the following to start the app server locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;poetry run uvicorn fastapi_neon.main:app &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app is live…locally! &lt;/p&gt;

&lt;p&gt;The application is now using Neon Postgres as the connected database. The &lt;code&gt;create_db_and_tables&lt;/code&gt; lifespan event created the todo model (table) in the database. You can double-check this by going into the Neon console and viewing the tables in your Neon project – you will see &lt;code&gt;todo&lt;/code&gt; listed.&lt;br&gt;&lt;br&gt;
You can insert and fetch &lt;em&gt;todos&lt;/em&gt; from the API by using the live endpoints. For example, try sending some requests using the built-in FastAPI Swagger UI docs endpoint at &lt;code&gt;http://0.0.0.0:8000/docs&lt;/code&gt;.&lt;/p&gt;



&lt;p&gt;Now, it’s time to deploy the app to AWS App Runner.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting up the AWS App Runner configuration
&lt;/h2&gt;

&lt;p&gt;A few items need to be in place before the app can be deployed to App Runner. Mainly, the database connection string will need to be stored in AWS Systems Manager (SSM) Parameter Store. Then, an &lt;strong&gt;&lt;em&gt;apprunner.yaml&lt;/em&gt;&lt;/strong&gt; configuration file will be added to the project that instructs App Runner how to build and run the app. This is also where the database connection string variable is referenced and associated with the app.&lt;/p&gt;

&lt;p&gt;At a high-level, the steps are as follows. Subsequent sections will guide you through each of these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the &lt;strong&gt;&lt;em&gt;apprunner.yaml&lt;/em&gt;&lt;/strong&gt; configuration file in the root of the project directory&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;DATABASE_URL&lt;/code&gt; parameter in SSM Parameter Store&lt;/li&gt;
&lt;li&gt;Update the &lt;strong&gt;&lt;em&gt;apprunner.yaml&lt;/em&gt;&lt;/strong&gt; secret value with the SSM parameter ARN to inject the database connection string into the runtime environment&lt;/li&gt;
&lt;li&gt;Create an &lt;em&gt;instance role&lt;/em&gt; that App Runner can use to access the SSM Parameter Store Secure String secret&lt;/li&gt;
&lt;li&gt;Push to GitHub or BitBucket and set up the deployment in the AWS App Runner console&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Create the App Runner &lt;em&gt;apprunner.yaml&lt;/em&gt; configuration
&lt;/h3&gt;

&lt;p&gt;In your project directory, create an &lt;strong&gt;&lt;em&gt;apprunner.yaml&lt;/em&gt;&lt;/strong&gt; configuration file with the following structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.0&lt;/span&gt;
&lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python311&lt;/span&gt;
&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Build command..."&lt;/span&gt;
&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runtime-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.11&lt;/span&gt;
  &lt;span class="na"&gt;pre-run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Installing dependencies..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pip3 install poetry&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;poetry config virtualenvs.create &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;poetry install&lt;/span&gt;

  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;poetry run uvicorn fastapi_neon.main:app --host 0.0.0.0 --port &lt;/span&gt;&lt;span class="m"&gt;8000&lt;/span&gt;
  &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
  &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;
      &lt;span class="na"&gt;value-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;replace-with-param-arn&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration instructs App Runner to install, build, and run the app. The specified managed runtime is the &lt;a href="https://docs.aws.amazon.com/apprunner/latest/dg/service-source-code.html#service-source-code.build-detail" rel="noopener noreferrer"&gt;revised Python 3.11 App Runner build&lt;/a&gt;. When the app is pulled from the git repo, the dependencies defined in the &lt;strong&gt;&lt;em&gt;poetry.loc&lt;/em&gt;&lt;/strong&gt; k file are installed, then the app is served using the same &lt;code&gt;uvicorn&lt;/code&gt; command from the local environment. You’ll notice that there is a placeholder for the &lt;code&gt;value-from&lt;/code&gt; entry in the &lt;strong&gt;secrets&lt;/strong&gt; sections. We’ll update this next.&lt;/p&gt;

&lt;p&gt;_Note: The App Runner CI process is very strict when parsing the configuration. Make sure to check the indentation and that the file name is not abbreviated and ends with &lt;strong&gt;.yaml&lt;/strong&gt;. _&lt;/p&gt;

&lt;h4&gt;
  
  
  Create a DATABASE_URL parameter in SSM Parameter Store
&lt;/h4&gt;

&lt;p&gt;Go to &lt;a href="http://console.aws.amazon.com/systems-manager/" rel="noopener noreferrer"&gt;AWS Systems Manager&lt;/a&gt; and navigate to &lt;strong&gt;Application Management&lt;/strong&gt; &amp;gt; &lt;strong&gt;Parameter Store&lt;/strong&gt; and click &lt;strong&gt;Create Parameter.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Enter the following for Name and Type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name:&lt;/strong&gt; &lt;code&gt;/fastapi-neon/DATABASE_URL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; SecureString&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the value, use the database connection URL from &lt;strong&gt;your Neon dashboard&lt;/strong&gt; that you’ve tested with previously.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create parameter&lt;/strong&gt;. Once created, click on the newly created parameter and copy the ARN identifier. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fneondatabase.wpengine.com%2Fwp-content%2Fuploads%2F2024%2F02%2Fssm-copy-arn-1024x770.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fneondatabase.wpengine.com%2Fwp-content%2Fuploads%2F2024%2F02%2Fssm-copy-arn-1024x770.png" alt="SSM Parameter Store - Copy parameter ARN&amp;lt;br&amp;gt;
" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, replace the placeholder (&lt;code&gt;&amp;lt;replace-with-param-arn&amp;gt;&lt;/code&gt;)value in &lt;strong&gt;&lt;em&gt;apprunner.yaml&lt;/em&gt;&lt;/strong&gt; in &lt;strong&gt;secrets&lt;/strong&gt; &amp;gt; &lt;strong&gt;value-from&lt;/strong&gt; with this ARN.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;
     &lt;span class="na"&gt;value-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;replace-with-param-arn&amp;gt;"&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;strong&gt;save&lt;/strong&gt;. &lt;/p&gt;

&lt;h4&gt;
  
  
  Push to Source Control
&lt;/h4&gt;

&lt;p&gt;The final project directory structure will be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;.&lt;/span&gt;
&lt;span class="s"&gt;├── fastapi_neon&lt;/span&gt;
&lt;span class="s"&gt;│ ├── __init__.py&lt;/span&gt;
&lt;span class="s"&gt;│ ├── main.py&lt;/span&gt; 
&lt;span class="s"&gt;│ └── settings.py&lt;/span&gt;
&lt;span class="s"&gt;├── tests/&lt;/span&gt;
&lt;span class="s"&gt;├── apprunner.yaml&lt;/span&gt;
&lt;span class="s"&gt;├── poetry.lock&lt;/span&gt;
&lt;span class="s"&gt;├── pyproject.toml&lt;/span&gt;
&lt;span class="s"&gt;├── README.md&lt;/span&gt;
&lt;span class="s"&gt;├── .gitignore&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm that &lt;code&gt;.env*&lt;/code&gt; is included in the &lt;strong&gt;&lt;em&gt;.gitignore&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;file&lt;/em&gt; in the root of the project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Environments&lt;/span&gt;
&lt;span class="s"&gt;.env*&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit and push the code to a repo in GitHub or BitBucket. Remember, &lt;strong&gt;do not commit the &lt;em&gt;.env&lt;/em&gt; file&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Create an App Runner instance role
&lt;/h4&gt;

&lt;p&gt;In order for the App Runner process to access the SSM Parameter Store secret, we’ll need to create an &lt;a href="https://docs.aws.amazon.com/apprunner/latest/dg/security_iam_service-with-iam.html" rel="noopener noreferrer"&gt;instance role&lt;/a&gt; with the appropriate permissions to access the SSM resource and attach it to the App Runner service when we create the service.&lt;/p&gt;

&lt;p&gt;Go to the &lt;a href="http://console.aws.amazon.com/iam/home" rel="noopener noreferrer"&gt;AWS Identity and Access Management (IAM)&lt;/a&gt; console and click &lt;strong&gt;Create role&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Select &lt;strong&gt;Custom trust policy&lt;/strong&gt; and add the JSON policy below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tasks.apprunner.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Next&lt;/strong&gt; and skip adding permissions&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Next&lt;/strong&gt; and enter the below information

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Role name&lt;/strong&gt; : &lt;em&gt;&lt;code&gt;app-runner-fastapi-role&lt;/code&gt;&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;Click &lt;strong&gt;Create Role&lt;/strong&gt;
&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;Now, &lt;strong&gt;open&lt;/strong&gt; the new &lt;em&gt;&lt;code&gt;app-runner-fastapi-role&lt;/code&gt;&lt;/em&gt;. Select &lt;strong&gt;Create&lt;/strong&gt;  &lt;strong&gt;inline policy&lt;/strong&gt; in the &lt;strong&gt;Permissions&lt;/strong&gt; tab.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fneondatabase.wpengine.com%2Fwp-content%2Fuploads%2F2024%2F02%2Fcreate-inline-policy-1024x205.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fneondatabase.wpengine.com%2Fwp-content%2Fuploads%2F2024%2F02%2Fcreate-inline-policy-1024x205.png" alt="AWS IAM - Create inline policy&amp;lt;br&amp;gt;
" width="800" height="160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the JSON editor, add the following permission policy with the parameter &lt;code&gt;/fastapi-neon/DATABASE_URL&lt;/code&gt; ARN from above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ssm:GetParameters"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;add-the-param-arn&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above permissions allow the App Runner &lt;a href="https://docs.aws.amazon.com/apprunner/latest/dg/env-variable.html" rel="noopener noreferrer"&gt;instance role to access the SSM parameter&lt;/a&gt;(s) that contains the database connection string.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt;. In the &lt;strong&gt;Policy details&lt;/strong&gt; section, add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Policy name&lt;/strong&gt; : &lt;em&gt;apprunner-ssm-policy&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, click &lt;strong&gt;Create policy&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying the application to AWS App Runner
&lt;/h2&gt;

&lt;p&gt;Finally, we are in App Runner. Let’s deploy this service!&lt;/p&gt;

&lt;p&gt;Go to &lt;a href="http://console.aws.amazon.com/apprunner/home" rel="noopener noreferrer"&gt;AWS App Runner&lt;/a&gt; and click &lt;strong&gt;Create an App Runner service&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F54tp5oid486s2q28am31.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F54tp5oid486s2q28am31.png" alt="AWS App Runner - Create Service" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This service will use a &lt;strong&gt;Source code repository&lt;/strong&gt; since it will be pulling the code from the repo that was just created. You will need to allow App Runner to access the repo in your account if you have not already authorized the AWS Connector.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flcokznfncam4l9h4qn4t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flcokznfncam4l9h4qn4t.png" alt="AWS App Runner - Configure Service" width="800" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select your git provider and repository details. In &lt;strong&gt;Deployment settings&lt;/strong&gt; , &lt;strong&gt;Automatic&lt;/strong&gt; is selected to deploy the app on each push to the linked branch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7lhjya7f6ru7n33bdfh2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7lhjya7f6ru7n33bdfh2.png" alt="AWS App Runner - Automatically deploy app" width="800" height="689"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt;. On the next screen, select &lt;strong&gt;Use a configuration file&lt;/strong&gt;. This will instruct App Runner to use the &lt;strong&gt;&lt;em&gt;apprunner.yaml&lt;/em&gt;&lt;/strong&gt; configuration file from the repo. Click &lt;strong&gt;Next&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;Configure service&lt;/strong&gt; section:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a &lt;strong&gt;Service name&lt;/strong&gt; for your app in Service settings&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Security&lt;/strong&gt; , select the instance role that was created earlier. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvui51uxr9vzc1k6i9kr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvui51uxr9vzc1k6i9kr.png" alt="AWS App Runner - Select instance role" width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The trust relationship that was specified during role creation is what determines if it is available in the dropdown options 🙂.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Next.&lt;/strong&gt; Next, review the setup and click &lt;strong&gt;Create &amp;amp; deploy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This may take a few minutes to deploy…&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once successfully deployed, test the FastAPI service at &lt;code&gt;https://&amp;lt;service-id&amp;gt;.&amp;lt;region&amp;gt;.awsapprunner.com/docs&lt;/code&gt;. For example, if you add data to the table using the SQL Editor in the Neon console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;todo&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="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Task 1.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;todo&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="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Task 2.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;todo&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="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Task 3.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you can fetch that data by accessing &lt;code&gt;https://&amp;lt;service-id&amp;gt;.&amp;lt;region&amp;gt;.awsapprunner.com/todos/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clean up
&lt;/h3&gt;

&lt;p&gt;To wrap up, delete the application if it’s no longer needed. To do this, go into the App Runner service and select &lt;strong&gt;Actions&lt;/strong&gt; &amp;gt; &lt;strong&gt;Delete&lt;/strong&gt; to remove the app. &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Integrating AWS App Runner and Neon Serverless Postgres is an effective way to deploy a robust, scalable serverless FastAPI service. Neon’s unique autoscaling capabilities ensure that your database resources adapt to your application’s demands without manual intervention. This dynamic scalability is crucial for maintaining optimal performance and cost efficiency during off-peak times and traffic bursts. This is perfect for growing teams that want to increase their development velocity while keeping their architecture flexible.&lt;/p&gt;

&lt;p&gt;To get started with incorporating Serverless Postgres into your FastAPI and Python apps, &lt;a href="https://console.neon.tech/signup" rel="noopener noreferrer"&gt;sign up and try Neon for free&lt;/a&gt;. And, join us in our &lt;a href="https://neon.tech/discord" rel="noopener noreferrer"&gt;Discord server&lt;/a&gt; to share your experiences, suggestions, and challenges. &lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The code for this guide is in the &lt;a href="https://github.com/neondatabase/fastapi-apprunner-neon" rel="noopener noreferrer"&gt;fastapi-apprunner-neon&lt;/a&gt; repo on GitHub&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>fullstack</category>
      <category>python</category>
      <category>containers</category>
    </item>
    <item>
      <title>Deploying a FastAPI backend using AWS Amplify Container-based REST APIs</title>
      <dc:creator>siegerts</dc:creator>
      <pubDate>Thu, 13 May 2021 21:17:35 +0000</pubDate>
      <link>https://forem.com/siegerts/deploying-a-fastapi-backend-using-aws-amplify-container-based-rest-apis-2f6i</link>
      <guid>https://forem.com/siegerts/deploying-a-fastapi-backend-using-aws-amplify-container-based-rest-apis-2f6i</guid>
      <description>&lt;p&gt;No surprise that it's a difficult process to go from development to production deployments in an organization 🤷. &lt;/p&gt;

&lt;p&gt;This is especially true for analytics and data science teams developing models or business logic. There's usually not a clear path or tooling to support application hand-offs or deployments across teams.&lt;/p&gt;

&lt;p&gt;One way to bridge the gap between applications is to allow for service-to-service communication. REST APIs are great for this type of use-case. Especially when auto-generated, interactive documentation, is available out-of-the-box 😉.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Amplify provides a great option for this - Serverless containers using API Gateway + AWS Fargate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This guide will follow the steps outlined in the &lt;a href="https://docs.amplify.aws/cli/usage/containers" rel="noopener noreferrer"&gt;Serverless containers&lt;/a&gt; section of the Amplify documentation and the &lt;a href="https://fastapi.tiangolo.com/deployment/docker/" rel="noopener noreferrer"&gt;FastAPI Docker Deployment&lt;/a&gt; documentation to quickly create and deploy a production-ready, scalable REST API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;You'll need to have the below installed and configured.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.amplify.aws/cli/start/install#install-the-amplify-cli" rel="noopener noreferrer"&gt;AWS Amplify CLI&lt;/a&gt; - working with Amplify (I'm using &lt;code&gt;v4.50.2&lt;/code&gt; )&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.docker.com/products/docker-desktop" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; - testing the APIs locally&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  FastAPI
&lt;/h3&gt;

&lt;p&gt;As mentioned above, we'll use FastAPI as the backend framework. The application pattern will feel natural if you're familiar with &lt;a href="https://flask.palletsprojects.com/en/2.0.x/" rel="noopener noreferrer"&gt;Flask&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;FastAPI has quickly become a go-to framework for setting up APIs for data science and analytics-based workloads. From the project page: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Among some of the callouts:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Fast: Very high performance, on par with NodeJS and Go&lt;/em&gt; &lt;br&gt;
&lt;em&gt;Robust: Get production-ready code. With automatic interactive documentation.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a great option for data science teams that want to focus on productionalizing APIs with the ability to scale in the future if needed.&lt;/p&gt;

&lt;p&gt;Also, as we'll see below, testing locally is a smooth process using Docker.&lt;/p&gt;

&lt;h3&gt;
  
  
  Serverless containers with Amplify
&lt;/h3&gt;

&lt;p&gt;If you need an escape hatch from just using Lambda then this may be a great option for you. We'll walk through a more basic set up below but the same approach can be leveraged for more complex backends using a &lt;strong&gt;docker-compose.yml&lt;/strong&gt; configuration.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Serverless containers provide the ability for you to deploy APIs and host websites using AWS Fargate. Customers with existing applications or those who require a lower level of control can bring Docker containers and deploy them into an Amplify project fully integrating with other resources.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Project set up
&lt;/h2&gt;

&lt;p&gt;Okay, let's deploy an API.&lt;/p&gt;

&lt;p&gt;First, create a new directory and initialize the the Amplify project. In this example, I'm not hooking up a frontend so I just select the default options. &lt;strong&gt;But&lt;/strong&gt;, if a frontend is something that you're interested in, then it's possible to layer that on and have the UI (React, Vue, etc.) talk to the backend API that is being created.&lt;/p&gt;

&lt;p&gt;Make the directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; &lt;span class="nb"&gt;mkdir &lt;/span&gt;fastapi-amplify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize the Amplify project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;amplify init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;
  Terminal workflow
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scanning for plugins...
Plugin scan successful
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project fastapiamplify
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using none
? Source Directory Path:  src
? Distribution Directory Path: dist
? Build Command:  npm run-script build
? Start Command: npm run-script start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling container-based deployments
&lt;/h2&gt;

&lt;p&gt;Now, with the project initialized, we need to enable the use of the container-based deployment options.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;amplify configure project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6vf5easxukau9cci35oz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6vf5easxukau9cci35oz.png" alt="Alt Text" width="800" height="85"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;
  Terminal workflow
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;? Which setting do you want to configure? Advanced: Container-based deployments
Using default provider  awscloudformation
? Do you want to enable container-based deployments? Yes

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding the API
&lt;/h2&gt;

&lt;p&gt;With the project configured, let's add the soon-to-be API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;amplify add api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8i7rjcolbs8l229sxyx4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8i7rjcolbs8l229sxyx4.png" alt="Alt Text" width="800" height="129"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;
  Terminal workflow
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;? Please select from one of the below mentioned services: REST
? Which service would you like to use API Gateway + AWS Fargate (Container-based)
? Provide a friendly name for your resource to be used as a label for this category in the project: fastapirest
? What image would you like to use Custom (bring your own Dockerfile or docker-compose.yml)
? When do you want to build &amp;amp; deploy the Fargate task On every "amplify push" (Fully managed container source)
? Do you want to restrict API access No
Successfully added resource fastapirest locally.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;/p&gt;

&lt;p&gt;The CLI will output some next steps for setting up the API to use a bring-your-own-container setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Next steps:
- Place your Dockerfile, docker-compose.yml and any related container source files in "amplify/backend/api/fastapirest/src"
- Amplify CLI infers many configuration settings from the "docker-compose.yaml" file. Learn more: docs.amplify.aws/cli/usage/containers
- Run "amplify push" to build and deploy your image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Following the above steps, the following &lt;strong&gt;app/&lt;/strong&gt; and &lt;strong&gt;Dockerfile&lt;/strong&gt; are added to the API directory in the Amplify backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  .
  ├── amplify/backend/api/&amp;lt;api-name&amp;gt;/src/
&lt;span class="gi"&gt;+ │   └── app
+ │       └── main.py
+ └────── Dockerfile
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding the Dockerfile
&lt;/h2&gt;

&lt;p&gt;This can be modified based on the needs and requirements of the project but the below default &lt;strong&gt;Dockerfile&lt;/strong&gt; will get you up-and-running immediately with FastAPI.  &lt;/p&gt;

&lt;p&gt;I've adjusted the example in the FastAPI docs to use &lt;code&gt;python3.8&lt;/code&gt; instead of &lt;code&gt;python3.7&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; added the statement to expose port 80.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; tiangolo/uvicorn-gunicorn-fastapi:python3.8&lt;/span&gt;

&lt;span class="c"&gt;# for Amplify &lt;/span&gt;
&lt;span class="c"&gt;# https://docs.amplify.aws/cli/usage/containers#deploy-a-single-container&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 80&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./app /app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure to add &lt;code&gt;EXPOSE 80&lt;/code&gt; to specify a port to communicate with the container. Amplify will suggest to use port 80 if you don't provide one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the API
&lt;/h2&gt;

&lt;p&gt;Now, we can add in the API logic in the &lt;strong&gt;main.py&lt;/strong&gt; file within &lt;strong&gt;app/&lt;/strong&gt;.&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="c1"&gt;# main.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_root&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;World&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/items/{item_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;item_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing locally
&lt;/h2&gt;

&lt;p&gt;With everything in place, we can test the API using Docker. Jump into the backend API directory (i.e. where the &lt;code&gt;Dockerfile&lt;/code&gt; is) and build the image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd amplify/backend/api/&amp;lt;api-name&amp;gt;/src/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build the image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker build -t fastapi .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, you can run the image locally, exposed on port 80 of your &lt;code&gt;localhost&lt;/code&gt; setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -d --name fastapi -p 80:80 fastapi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see the auto-generated API documentation at &lt;a href="http://127.0.0.1/redoc" rel="noopener noreferrer"&gt;http://127.0.0.1/redoc&lt;/a&gt; or &lt;a href="http://127.0.0.1/docs" rel="noopener noreferrer"&gt;http://127.0.0.1/docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying to Amplify 🚀
&lt;/h2&gt;

&lt;p&gt;Awesome! Now let's push this to Amplify.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;amplify push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When prompted, select &lt;code&gt;Yes&lt;/code&gt; to &lt;strong&gt;Create&lt;/strong&gt; the new resource.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;(🚀 dev) 🐶 ➜  fastapi-amplify amplify push                                                                                  (main) ✗
✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

| Category | Resource name | Operation | Provider plugin   |
| -------- | ------------- | --------- | ----------------- |
| Api      | fastapirest   | Create    | awscloudformation |
? Are you sure you want to continue? Yes


&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the process is complete. 👇&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✔ All resources are updated in the cloud

REST API endpoint: https://&amp;lt;id&amp;gt;.execute-api.&amp;lt;region&amp;gt;.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing the deployed API
&lt;/h2&gt;

&lt;p&gt;After being deployed, the API can be accessed using the endpoint above:&lt;/p&gt;

&lt;p&gt;Also, the live API documentation is live at the &lt;code&gt;https://&amp;lt;endpoint&amp;gt;/docs&lt;/code&gt; and &lt;code&gt;https://&amp;lt;endpoint&amp;gt;/redoc&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/docs&lt;/code&gt; route
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmm90ba156rd43tahh60o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmm90ba156rd43tahh60o.png" alt="Alt Text" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;/redoc&lt;/code&gt; route
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi15uizxwkq58plxqa79p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi15uizxwkq58plxqa79p.png" alt="Alt Text" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;This is just scratching the surface of configuration options. Hope this helps you get running quickly with serverless FastAPI backends!&lt;/p&gt;

&lt;p&gt;Subscribe to get my latest content by email -&amp;gt; &lt;a href="https://siegerts.ck.page/74222e7e60" rel="noopener noreferrer"&gt;Newsletter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>fastapi</category>
      <category>amplify</category>
    </item>
    <item>
      <title>Displaying the active Amplify Environment alongside the current Git branch</title>
      <dc:creator>siegerts</dc:creator>
      <pubDate>Mon, 10 May 2021 16:50:10 +0000</pubDate>
      <link>https://forem.com/siegerts/displaying-the-active-amplify-environment-alongside-the-current-git-branch-4hmg</link>
      <guid>https://forem.com/siegerts/displaying-the-active-amplify-environment-alongside-the-current-git-branch-4hmg</guid>
      <description>&lt;p&gt;Working with a lot of Git branches can get a bit tricky when they align to different &lt;a href="https://docs.amplify.aws/cli/teams/overview" rel="noopener noreferrer"&gt;Amplify environments&lt;/a&gt;. I usually find myself checking &lt;code&gt;amplify status&lt;/code&gt; or &lt;code&gt;amplify env list&lt;/code&gt; to determine the active environment. Below is an approach that is a bit more dynamic and similar to what Git and Python virtual environments show while working in the terminal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;You'll need to have the below installed for the function to take effect.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/" rel="noopener noreferrer"&gt;git&lt;/a&gt; - used to determine the root directory of your Amplify project&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://stedolan.github.io/jq/download/" rel="noopener noreferrer"&gt;jq&lt;/a&gt; - helps with parsing JSON&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ohmyz.sh/" rel="noopener noreferrer"&gt;oh-my-zsh&lt;/a&gt; - managing terminal functions and themes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Display the active Amplify &lt;code&gt;env&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Add the below into your &lt;code&gt;.zshrc&lt;/code&gt; or &lt;code&gt;.bashrc&lt;/code&gt;. The output of the function can be adjusted depending on how you'd like to display the information. I display mine to the left of the current working directory by adjusting my zsh custom theme (below). This puts the environment name in a similar spot to where an active Python virtual environment name will show.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .zshrc&lt;/span&gt;

amplify_env &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;PROJECT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--show-toplevel&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; 

    &lt;span class="nv"&gt;ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_DIR&lt;/span&gt;/amplify/.config/local-env-info.json 

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ENV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nv"&gt;env_info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="nv"&gt;$ENV&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;".envName"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; 
        &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"(🚀 &lt;/span&gt;&lt;span class="nv"&gt;$env_info&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
    &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function checks for the current working environment name in the &lt;code&gt;&amp;lt;project&amp;gt;/amplify/.config/local-env-info.json&lt;/code&gt; file. This approach requires a few extra steps but is quicker than re-computing on each terminal input using the &lt;code&gt;amplify status&lt;/code&gt; CLI command. &lt;/p&gt;

&lt;p&gt;After re-sourcing your &lt;code&gt;.zshrc&lt;/code&gt; (below), the environment function can be invoked by running &lt;code&gt;amplify_env&lt;/code&gt; in the terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Adding to a custom theme
&lt;/h3&gt;

&lt;p&gt;If you're using &lt;code&gt;oh-my-zsh&lt;/code&gt;, then you can adjust your theme (or any theme) to add the output of the function so that it shows in the terminal prompts. The updated &lt;code&gt;PROMPT&lt;/code&gt; below now includes the Amplify environment function output (&lt;code&gt;${amplify_env}&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;# ~/.oh-my-zsh/custom/themes/&amp;lt;my-custom-theme&amp;gt;.zsh-theme
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- PROMPT='${ret_status} %{$fg[cyan]%}%c%{$reset_color%} '
&lt;/span&gt;&lt;span class="gi"&gt;+ PROMPT='$(amplify_env) ${ret_status} %{$fg[cyan]%}%c%{$reset_color%} '
&lt;/span&gt;...
&lt;span class="err"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Amplify &lt;code&gt;env&lt;/code&gt; + Git branch = 🔥
&lt;/h3&gt;

&lt;p&gt;Now the environment is displayed alongside the Git branch that is active. This is a nice way to quickly determine if I need to switch branches or environments to match. Make sure to initialize Git in the project(&lt;code&gt;git init&lt;/code&gt;) if the environment name and the Git branch aren't showing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5ubwg353cyb5ni0p00d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5ubwg353cyb5ni0p00d.png" alt="Alt Text" width="800" height="124"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hopefully that helps keep your Amplify environments visually in sync with your current Git branch 🌲.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>amplify</category>
      <category>environments</category>
      <category>serverless</category>
    </item>
  </channel>
</rss>
