<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kevin Cole</title>
    <description>The latest articles on DEV Community by Kevin Cole (@kcole93).</description>
    <link>https://dev.to/kcole93</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%2F1185698%2Fad7adffe-73ff-4166-bc7b-977f77462776.png</url>
      <title>DEV Community: Kevin Cole</title>
      <link>https://dev.to/kcole93</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kcole93"/>
    <language>en</language>
    <item>
      <title>'Aw CRUD!' Enable Your Custom GPT to Talk to Any Airtable Base</title>
      <dc:creator>Kevin Cole</dc:creator>
      <pubDate>Thu, 16 Nov 2023 21:48:39 +0000</pubDate>
      <link>https://dev.to/kcole93/aw-crud-enable-your-custom-gpt-to-talk-to-any-airtable-base-4pfl</link>
      <guid>https://dev.to/kcole93/aw-crud-enable-your-custom-gpt-to-talk-to-any-airtable-base-4pfl</guid>
      <description>&lt;p&gt;Last week, OpenAI announced a number of new models, features and pricing changes to their suite of tools. Among these announcements was the introduction of "Custom GPTs" which can be fed specific instructions and documents. While these features offer a convenient no-code alternative to assembling your own chain of LLM tools and agents, perhaps the most impactful feature is the ability to easily provide your Custom GPTs with access to "Actions": third-party integrations accessed via an API.&lt;/p&gt;

&lt;p&gt;In this post, I'll provide step-by-step instructions for connecting your custom GPTs to any Airtable base you own, enabling your GPT to find and read records, update or create new records, and delete existing records.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; In order to follow this tutorial, you'll need an OpenAI subscription (to access the Custom GPTs beta feature) and an Airtable account (free or paid).&lt;/p&gt;

&lt;h2&gt;
  
  
  Initializing Your Custom GPT
&lt;/h2&gt;

&lt;p&gt;For this tutorial, we'll build a Custom GPT that helps us track interesting GitHub projects by adding them to our Airtable Base &lt;a href="https://airtable.com/appKjP9iTJnUvFJXj/shrLrIfi7KDF7ehD8"&gt;"Cool Projects"&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Of course, given the combined flexibility of Airtable and ChatGPT, there are few limits on the kinds of projects you can build by combining these two tools. You could, for example, use an Airtable base as a kind of long-term memory bank for your Custom GPT, as a bare bones &lt;strong&gt;R&lt;/strong&gt;etrieval &lt;strong&gt;A&lt;/strong&gt;ugmented &lt;strong&gt;G&lt;/strong&gt;eneration (RAG) tool, etcetera.&lt;/p&gt;

&lt;p&gt;To start, ensure you're logged into your OpenAI account and visit the &lt;a href="https://chat.openai.com/gpts/discovery"&gt;'Explore'&lt;/a&gt; page. There, click "Create a GPT" to begin configuring your custom GPT.&lt;/p&gt;

&lt;p&gt;While you can interact with the "Create" dialog to set up some of the GPT's basic parameters, I don't recommend doing so because it's quite easy to hit the current 50 messages/hour cap for GPT-4. Instead, click on the 'Configure' tab.&lt;/p&gt;

&lt;p&gt;We'll name our GPT 'Project Tracker', provide a description and a short set of instructions.&lt;/p&gt;

&lt;p&gt;We can refine the &lt;strong&gt;Instructions&lt;/strong&gt; later, so for now we'll just enter something straightforward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Project Tracker is a friendly assistant which uses its set of Actions to interact with records in the 'Cool Projects' Airtable Base.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At this point, we can decide which &lt;strong&gt;Capabilities&lt;/strong&gt; to provide our custom GPT. We'll definitely want to enable Web Browsing, since we'll ask the GPT to visit and summarize GitHub repositories by providing it with links. I'll skip DALL-E Image Generation for now, but enable the Code Interpreter to allow us to do some cool analysis tasks using our Airtable data.&lt;/p&gt;

&lt;p&gt;Let's also generate a nice icon for the custom GPT. To do so, I'll switch back to the 'Create' tab and prompt the GPT Builder.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; Generate a pixel art illustration of a lightbulb to use as my GPT's icon.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5OfFqrj_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j2mjqipa3czgjqndv47w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5OfFqrj_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j2mjqipa3czgjqndv47w.png" alt="A screenshot of the Custom GPT Configuration screen showing the settings described above." width="800" height="729"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;We'll start of with this basic configuration.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Don't forget to save your changes so far!&lt;/p&gt;

&lt;p&gt;Now that we've initialized our Project Tracker GPT, we'll need to turn to Airtable to generate instructions on how our GPT can interface with the Airtable API and our Airtable Base.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating an OpenAPI Schema from Our Airtable Base
&lt;/h2&gt;

&lt;p&gt;For the purposes of this demonstration, I've created a publicly accessible Airtable Base called &lt;a href="https://airtable.com/appKjP9iTJnUvFJXj/shrLrIfi7KDF7ehD8"&gt;"Cool Projects"&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To get the best performance out of your custom GPT, there are a few things I'd recommend. First, make use of Airtable's field descriptions to provide semantically rich descriptions of the purpose of each field. We'll pass these field descriptions to the custom GPT when defining our Actions, and while I can't confirm that this is the case, providing further context to the GPT about how each field is used seems to improve performance.&lt;/p&gt;

&lt;p&gt;To start, I'll add a few of my own GitHub projects manually. This will provide some context for the custom GPT regarding how we expect the data to be stored and formatted. While you &lt;em&gt;could&lt;/em&gt; start with an empty base and have your GPT add all the records to it, I find it performs best when there is pre-existing content to model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating an OpenAPI Schema
&lt;/h3&gt;

&lt;p&gt;OpenAI's Actions API enables custom GPTs to interface with third-party applications through those platforms' &lt;strong&gt;A&lt;/strong&gt;pplication &lt;strong&gt;P&lt;/strong&gt;rogramming &lt;strong&gt;I&lt;/strong&gt;nterfaces (APIs). To instruct the GPT on how to interact with this API, the Custom GPT Editor accepts an &lt;a href="https://www.openapis.org/"&gt;OpenAPI&lt;/a&gt; schema, a standardized format for defining how an API expects to be interacted with.&lt;/p&gt;

&lt;p&gt;In order to enable our custom GPT to interact with our Airtable base, we'll have to provide our own OpenAPI schema. Luckily, we don't need to generate this schema by hand!&lt;/p&gt;

&lt;p&gt;I've created a script for Airtable's  Scripting extension, adapting the (very well implemented!) work of GitHub user &lt;a href="https://github.com/TheF1rstPancake/AirtableOpenAPICustomBlock"&gt;TheF1rstPancake&lt;/a&gt; on a custom Airtable Block that generates an OpenAPI specification from any Airtable base, which can instantly generate this schema.&lt;/p&gt;

&lt;p&gt;To use it, we'll need to add the &lt;strong&gt;Scripting&lt;/strong&gt; extension to our base (&lt;strong&gt;Extensions&lt;/strong&gt; → &lt;strong&gt;+ Add an extension&lt;/strong&gt; → &lt;strong&gt;Scripting&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;We'll edit the script, deleting the boilerplate code and replacing it with our own script, which you can find in &lt;a href="https://gist.github.com/kcole93/5d86cfe6fbcec60cc5ece6e2246158d6"&gt;this GitHub Gist&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After pasting the code into the Script Editor, click &lt;strong&gt;Run&lt;/strong&gt; and you'll be presented with a full OpenAPI schema for your Airtable Base. This schema will allow you to grant your custom GPT with full &lt;strong&gt;C&lt;/strong&gt;reate &lt;strong&gt;R&lt;/strong&gt;eplace &lt;strong&gt;U&lt;/strong&gt;pdate &lt;strong&gt;D&lt;/strong&gt;estroy (CRUD) access to your base. However, before we finish defining our GPT's Actions, we'll need need to create a &lt;strong&gt;P&lt;/strong&gt;ersonal &lt;strong&gt;A&lt;/strong&gt;ccess &lt;strong&gt;T&lt;/strong&gt;oken (PAT) which provides our custom GPT with the necessary permission scopes to interact with our Airtable base via the Airtable API, and which we'll use to authenticate the custom GPT agent.&lt;/p&gt;

&lt;h4&gt;
  
  
  🚧 Warning 🚧
&lt;/h4&gt;

&lt;p&gt;By default, the OpenAPI schema includes &lt;em&gt;all&lt;/em&gt; methods of interacting with the Airtable API, &lt;strong&gt;including destructive methods like PATCH and DELETE&lt;/strong&gt;.  It also includes &lt;strong&gt;all of the specified base's tables&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This means that if you, for example, were to make a custom GPT and provide it with your OpenAPI schema, and subsequently shared that custom GPT with others, &lt;strong&gt;anyone&lt;/strong&gt; could instruct the GPT to perform destructive operations on your Airtable base, like deleting all of its records or writing over existing records! Please keep this in mind when developing and sharing your custom GPTs. &lt;/p&gt;

&lt;p&gt;You can limit the operations available to the GPT, but you'll need to manually remove their corresponding "path" definitions from the generated schema (for example, using your favorite text editor) before defining your GPT's Actions.&lt;sup id="fnref1"&gt;1&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;It's both possible &lt;em&gt;and recommended&lt;/em&gt; to limit the scope of the GPT's permissions when generating its Airtable PAT; however, I wouldn't recommend only using permissions scope limitations to restrict your GPT's capabilities, as it will attempt to use any defined Actions and report authentication/permission errors in the chat output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating a Personal Access Token for Your Custom GPT
&lt;/h3&gt;

&lt;p&gt;Since Airtable is deprecating API keys at the end of January 2024, we'll need to create a Personal Access Token instead.&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;To create a PAT to authenticate with and access the Airtable API, visit &lt;a href="https://airtable.com/create/tokens"&gt;https://airtable.com/create/tokens&lt;/a&gt;, and click &lt;strong&gt;Create new token&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Give this PAT a meaningful name, and assign its scope of permissions according to your needs. For this demonstration, I'll enable full read/write capabilities so that my custom GPT can add new records and update existing ones.&lt;/p&gt;

&lt;p&gt;Be sure to carefully select the scope of access for this PAT. You'll want to limit the scope to the single Base that you're working with.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rKHsCZy3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jdqp3bna2mkpnl81czhy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rKHsCZy3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jdqp3bna2mkpnl81czhy.png" alt="A screenshot demonstrating the creation of a PAT in the Airtable interface." width="800" height="586"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;It's worthwhile to give our PAT a meaningful name, because record revision histories in the Airtable interface will show this name to identify changes made by our custom GPT.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once you click &lt;strong&gt;Create token&lt;/strong&gt;, you'll be presented with the PAT. Be sure to copy this to a text file on your desktop, since we'll need to provide it to our custom GPT when setting up our actions. If you forget the PAT, you can regenerate it with the same settings from the Personal access token dashboard in Airtable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Airtable Access to Your Custom GPT
&lt;/h2&gt;

&lt;p&gt;To finish enabling your custom GPT to interface with your Airtable base, edit your GPT and open the &lt;strong&gt;Configure&lt;/strong&gt; pane. Click on &lt;strong&gt;Add Action&lt;/strong&gt;, and paste your generate OpenAPI Schema into the &lt;strong&gt;Schema&lt;/strong&gt; input. You should see a list of all of the actions that are generated from this schema.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PTOsHNaG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2eh7lkni8j675qzcmris.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PTOsHNaG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2eh7lkni8j675qzcmris.png" alt="A screenshot demonstrating pasting the generated OpenAPI schema into the Actions configuration for our custom GPT." width="800" height="822"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Under "Available actions" we now see a list of the different operations available for each table in our base.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As a final step, we need to set up our authentication configuration. Click on the cog icon by the &lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JyLL-PNm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jblkhgo8clntzqsc4dk1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JyLL-PNm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jblkhgo8clntzqsc4dk1.png" alt="A screenshot showing a test prompt to the Project Tracker GPT. Prompt: 'I'd like you to visit my GitHub repo and add any other pinned projects to my Airtable base's 'Projects' table: [link to author's GitHub profile] The GPT browses the provided link, makes a number of API calls, and reports that it has successfully added four new projects to the Airtable base." width="800" height="818"&gt;&lt;/a&gt;&lt;strong&gt;Authentication&lt;/strong&gt; input, and select &lt;strong&gt;API Key&lt;/strong&gt;. Paste your PAT into the &lt;strong&gt;API Key&lt;/strong&gt; field, and select "Bearer" as the &lt;strong&gt;Auth Type&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can go ahead and test our your Actions by clicking the &lt;strong&gt;Test&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---_kAjVdT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lr5cncytool10rpslawu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---_kAjVdT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lr5cncytool10rpslawu.png" alt="A screenshot showing the output after running a test of the 'Get Projects' operation. The Project Tracker GPT calls the HTTP endpoint for our API, retrieves information from our Airtable base, and summarizes that information in the chat response." width="800" height="818"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A test of our "Get Projects" operation demonstrates that our custom GPT is now able to read data in from our Airtable base!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And that's it! Your custom GPT now has full CRUD access to your Airtable base. Now, you can do things like ask your GPT to add new records to your base's tables:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--57LviuMk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/264o9ecsnwz6gk9oyg98.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--57LviuMk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/264o9ecsnwz6gk9oyg98.png" alt="A screenshot showing a test prompt to the Project Tracker GPT. Prompt: 'I'd like you to visit my GitHub repo and add any other pinned projects to my Airtable base's 'Projects' table: [link to author's GitHub profile] The GPT browses the provided link, makes a number of API calls, and reports that it has successfully added four new projects to the Airtable base." width="800" height="818"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Now you can use natural language to instruct your custom GPT to interact with your Airtable API.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips and Reflections
&lt;/h2&gt;

&lt;p&gt;While testing out the new Custom GPT feature, I've come across a few hiccups:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Out-of-date Action schemas are occasionally 'remembered' by your custom GPT, leading them to form incorrect calls to the Airtable API. A work around seems to be deleting your Action entirely, saving the custom GPT, and then re-adding your updated Action schema.&lt;/li&gt;
&lt;li&gt;Custom GPTs don't seem to hold the full OpenAPI schema in memory when deciding on actions. For example, I don't believe that the entire "schemas" specification is followed at all times, so occasionally the custom GPT will try to send malformed API calls or attempt ot push/patch malformed objects. It is responsive to feedback, at least!&lt;/li&gt;
&lt;li&gt;You can add additional information about how to best navigate/utilize the information in your Airtable base via the custom GPT's &lt;strong&gt;Instructions&lt;/strong&gt; field; however, GPTs don't always make 'informed' decisions about how to query or transform information received from outside sources. For example, if you ask for the GPT to retrieve a record, it may attempt to filter on the record's title and if there is no match and the API returns no results, it will complain that it wasn't able to find the record instead of searching all records and then conducting a more 'semantic' search on the results.&lt;/li&gt;
&lt;li&gt;There's a limit to how much information your custom GPT will ingest. As a result, you'll need to make strategic use of Airtable's Views feature if your use case involves a lot of records or records with long text data. The GPT generally attempts to limit returned information using the query parameters/filters specified in the Airtable API, but occasionally makes mistakes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By default, the "typecast" option in the Airtable API is disabled. This is meant to prevent the custom GPT from creating new options in Single/Multi-Select fields. However, if you would like to enable this behavior you can manually change the value of the "typecast" parameter in the OpenAPI specification.&lt;/p&gt;

&lt;p&gt;🔐 Another note: I'd recommend disabling sharing your chat with OpenAI to improve their services, especially if you're dealing with personal/sensitive data! To do so, deselect 'Use conversation data in your GPT to improve our models' under &lt;strong&gt;Additional Settings&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Footnotes
&lt;/h2&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;I may refactor the current script to enable users to specify which methods they would like to include/exclude from the generated schema, but currently this is not supported and the limitations of Airtable's Scripting runtime make adding this functionality somewhat painful. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;While it's not a best practice to assign a PAT to a third-party integration, there are a number of limitations which make it unfeasible to use Airtable for public-facing purposes, including for a publicly accessible custom GPT. There is a limit of 5 API requests per second per base, which makes concurrent connections essentially impossible with a user base of non-trivial size. For this reason, I've eschewed creating an OAuth integration, but you should consider doing so if you plan to share your GPT with others. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Cranberry: The Cure for Your URI</title>
      <dc:creator>Kevin Cole</dc:creator>
      <pubDate>Sat, 21 Oct 2023 05:33:21 +0000</pubDate>
      <link>https://dev.to/kcole93/cranberry-the-cure-for-your-uri-k90</link>
      <guid>https://dev.to/kcole93/cranberry-the-cure-for-your-uri-k90</guid>
      <description>&lt;p&gt;Everyone is familiar with the term URL. Even if you couldn't recall off the top of your head that this abbreviation stands for &lt;strong&gt;U&lt;/strong&gt;niform &lt;strong&gt;R&lt;/strong&gt;esource &lt;strong&gt;L&lt;/strong&gt;ocator, you'd be able to describe URLs as the addresses of specific websites that you put into your browser's address bar or, more frequently, click to via hyperlinks on other webpages.&lt;/p&gt;

&lt;p&gt;You may not be so familiar with &lt;strong&gt;U&lt;/strong&gt;niform &lt;strong&gt;R&lt;/strong&gt;esource &lt;strong&gt;I&lt;/strong&gt;dentifiers (URIs), a &lt;a href="https://en.wikipedia.org/wiki/List_of_URI_schemes"&gt;much broader syntax&lt;/a&gt; for identifying different resources across the web. For example, there are URI schemes which allow you to invoke a device's SMS messaging capabilities to compose a message, and perhaps a more familiar example are Spotify's URIs that allow you to open a specific song, artist or playlist in the desktop/mobile application via a link.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- An example of an SMS URI --&amp;gt;&lt;/span&gt;
sms:+15105550101?body=hello

&lt;span class="c"&gt;&amp;lt;!-- An example of the Spotify URI scheme --&amp;gt;&lt;/span&gt;
spotify:artist:7t0rwkOPGlDPEhaOcVtOt9

&lt;span class="c"&gt;&amp;lt;!-- An example of the Git protocol URI scheme --&amp;gt;&lt;/span&gt;
git://github.com/whatwg/url.git

&lt;span class="c"&gt;&amp;lt;!-- Zotero's URI Protocol --&amp;gt;&lt;/span&gt;
zotero://select/library/collections/{collection_key}/items/{item_key}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While URIs provide a ton of useful functionality, actually using them can be a bit difficult. Many of the places you'd think to use a hyperlink to provide clickable access to a URI refuse to validate the input URI. For example, it's (currently) not possible to provide a URI as the target of a link in the popular note taking application &lt;a href="https://notion.so"&gt;Notion.so&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That's always bothered me, especially because for my work I make heavy use of the research and citation manager &lt;a href="https://zotero.org"&gt;Zotero&lt;/a&gt;, which allows you to create URIs that, when opened, point you to a specific item in your library of saved materials. When working with a team of researchers and in a library of hundreds and even thousands of documents, being able to send someone one of these URIs is a huge time saver!&lt;/p&gt;

&lt;p&gt;As I was driving the other day I had a shockingly simple realization: why not make a micro-service which accepts a Zotero URI as a part of a traditional URL, and redirects the browser to that URI when opened? In part, I was inspired by the ease with which Gitpod allows you to append a repository's URL to their own web address in order to open the repository in Gitpod. And so, I set out to solve this uncomfortable URI woe. And thus was born: &lt;a href="https://cranberry.vercel.app/"&gt;Cranberry&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;Cranberry leverages the advantages of so-called &lt;a href="https://en.wikipedia.org/wiki/Edge_computing"&gt;'edge computing'&lt;/a&gt;, a paradigm focused on ensuring low-latency by performing computational manipulation as close to the data source and client as possible. This is made possible by hosting Cranberry via Vercel and utilizing their &lt;a href="https://vercel.com/docs/functions/edge-middleware"&gt;Edge Middleware&lt;/a&gt; to redirect requests containing a Zotero URI. I decided to use my favorite lightweight web development framework, &lt;a href="https://astro.build"&gt;Astro&lt;/a&gt;, although Vercel and its Edge Middleware services are compatible with a number of different frameworks. &lt;/p&gt;

&lt;p&gt;In this article, I'll walk provide a step-by-step walk-through documenting how to set up this type of micro-service and also reflect on some of the benefits and drawbacks of the approach taken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining Our Goals
&lt;/h2&gt;

&lt;p&gt;For this micro-service, we want to be able to do the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Serve a small homepage describing the project.&lt;/li&gt;
&lt;li&gt;Redirect any URL containing a Zotero URI to the specified URI. (We'll use a query parameter &lt;code&gt;?uri={USER_PROVIDED_URI}&lt;/code&gt; )&lt;sup id="fnref1"&gt;1&lt;/sup&gt;
&lt;/li&gt;
&lt;li&gt;Provide appropriate and understandable error messages.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's also helpful to define a few anti-goals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We don't want to redirect to user-provided URLs or other URI protocols, as doing so could make our service useful for bad actors trying to maliciously redirect users.&lt;sup id="fnref2"&gt;2&lt;/sup&gt;
&lt;/li&gt;
&lt;li&gt;We don't want to send HTML to the client unless useful (e.g., visitors to the homepage who haven't come via a link containing a URI or in case of errors).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Initializing Your Astro Project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a new project with npm&lt;/span&gt;
npm create astro@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Houston, Astro's adorable mascot, will guide you through the initial set up. Choose a name and location for your project, as well as whether or not to start with some basic scaffolding files and whether or not you plan to use Typescript. Let's use the basic project template, and in this project we'll use a mixture of both Javascript and Typescript.&lt;/p&gt;

&lt;p&gt;After your project initializes, be sure to change into the project directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling Server-Side Rendering
&lt;/h2&gt;

&lt;p&gt;As mentioned, I decided to use Server-side Rendering (SSR) for this project, both because I wanted to familiarize myself with this rendering mode, as well as to take advantage of a few benefits that Edge-enabled SSR integrations can offer us for this use case, namely:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Because the majority of requests to our website's servers are merely meant to instantly redirect to the provided URI, there's no need to actually serve any website content. We'll only want to serve HTML to users who are visiting the homepage, or in case of an error.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;By using an SSR host with a global Content Delivery Network (CDN) and Edge-powered middleware, we can further minimize latency, especially for users who would otherwise be further away from our server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Since we perform our operations in at the Edge, we can ensure that all user agents and browsers will be able to utilize our service in accordance to the principle of progressive enhancement. If we were to rely on client-side manipulation, the service would only work if Javascript is supported and enabled.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However, there's a flip side to this coin! For one, depending on a (company's) CDN network and using their proprietary Edge function service and runtime means that our website and its functionality is &lt;strong&gt;much less portable&lt;/strong&gt; than relying on more traditional middleware strategies or even taking a client-side approach.&lt;/p&gt;

&lt;p&gt;Since we're planning to deploy via Vercel, our next step will be to install and configure the Vercel SSR Adapter for Astro.&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;# This will install the adapter and update your astro config file&lt;/span&gt;
npx astro add vercel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you prefer to manually install dependencies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install via npm
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @astrojs/vercel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Update your &lt;code&gt;astro.config.mjs&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;defineConfig&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;astro/config&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="nx"&gt;vercel&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;@astrojs/vercel/serverless&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="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vercel&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;In order to test our edge middleware locally in development, we'll also need to install the Vercel CLI tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; vercel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting Up Our Middleware
&lt;/h2&gt;

&lt;p&gt;So, what exactly does 'middleware' do? In the context of web development, middleware describes computations and modifications done between the time a server receives an incoming request and the response to that request is issued. It's a kind of interception layer that allows for all kinds of important procedures like authenticating users before serving protected content, fetching data from databases or other APIs and including that data in the response, among many others.&lt;/p&gt;

&lt;p&gt;In our project, middleware will allow us intercept incoming requests (e.g., when someone clicks on a Cranberry-fied link or navigates to the homepage) and to decide on the appropriate response based on the data available in that request. More specifically, we'll want to evaluate whether or not the request contains a URI and, if so, whether or not the URI provided is 'valid' for our purposes (e.g., if the URI uses the &lt;code&gt;zotero://&lt;/code&gt; protocol).&lt;/p&gt;

&lt;p&gt;In other words, there should be three potential responses returned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Case One: No URI Included → show project homepage.&lt;/li&gt;
&lt;li&gt;Case Two: URI is included, and uses the Zotero protocol → immediately redirect to the provided URI.&lt;/li&gt;
&lt;li&gt;Case Three: URI is included, but is invalid → return error message and show error page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this plan in mind, let's set up our &lt;code&gt;middleware.js&lt;/code&gt; file which will be used by Vercel to set up Edge Middleware functions.&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;// Originally this function was defined locally, but we'll use it again later to validate user input on our homepage so I've encapsulated it and imported it here&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;isValidURI&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;./src/utils/isValidZoteroURI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// We use the config export to define a matcher pattern, which tells Vercel which paths to run our middlewear on&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;matcher&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;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;// Match the root path&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Here's where we define our middlewear functionality.&lt;/span&gt;
&lt;span class="c1"&gt;// The function should return a response, and is async&lt;/span&gt;
&lt;span class="c1"&gt;// so that we can fetch the Error page and return it in our response&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;middleware&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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Grab the incoming request's url&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&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;URL&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;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Then extract the URI appended to the url&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uriParam&lt;/span&gt; &lt;span class="o"&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;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uri&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// CASE ONE&lt;/span&gt;
    &lt;span class="c1"&gt;// If there's no URI, return null to make no change to the response&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;uriParam&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// This means a normal GET request will fetch the home page&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// CASE TWO&lt;/span&gt;
    &lt;span class="c1"&gt;// If theres a URI and it's valid, try to redirect&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;isValidZoteroURI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uriParam&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// See below for why we have to catch errors even after validating URIs&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;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uriParam&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Redirect to the URI&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="nx"&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;Redirection error:&lt;/span&gt;&lt;span class="dl"&gt;'&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;handleError&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to 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="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;handleError&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The URI provided is invalid.&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="c1"&gt;// Handle errors by returning a 400 status &amp;amp; displaying error page&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;handleError&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;errorMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Fetch the page created by `error.astro`&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorUrl&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/error&lt;/span&gt;&lt;span class="dl"&gt;'&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;origin&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;errorPageResponse&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errorUrl&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;errorPageContent&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;errorPageResponse&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="c1"&gt;// Serve our error page to provide direction for human users &lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errorPageContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;status&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="na"&gt;headers&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;Content-Type&lt;/span&gt;&lt;span class="dl"&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;text/html&lt;/span&gt;&lt;span class="dl"&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;X-Error-Message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;errorMessage&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;Let's take a look at the contents of &lt;code&gt;./src/utils/isValidZoteroURI.ts&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;isValidZoteroURI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// We grab the scheme (text before the first colon)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;// Return true if the protocol uses zotero's scheme&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zotero&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scheme&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="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Otherwise, return false&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, we should have functioning middleware to intercept and respond to incoming requests accordingly. All that's left is to put together the &lt;code&gt;.astro&lt;/code&gt; files to handle our front-end, and then to deploy to Vercel.&lt;/p&gt;

&lt;p&gt;You can test your middleware locally by running &lt;code&gt;vercel dev&lt;/code&gt; in your project's root directory -- but you'll need to have a valid HTML page returned at the endpoints &lt;code&gt;/index.html&lt;/code&gt; and &lt;code&gt;error.html&lt;/code&gt;, otherwise the middleware function will throw a 404 error. If you'd like to see how I put together my minimal frontend, take a look at Cranberry's &lt;a href=""&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To test in a staging deployment, run &lt;code&gt;vercel deploy&lt;/code&gt;. By deploying to a staging environment, you'll be able to see your Vercel Edge/Serverless function logs for debugging.&lt;/p&gt;

&lt;p&gt;When you're happy with the way things are looking and working, you can deploy to production with &lt;code&gt;vercel deploy --prod&lt;/code&gt;.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;I originally planned to use the shorter URL fragment syntax (&lt;code&gt;#&lt;/code&gt;), but this approach only works for client-side manipulation, as URL fragments aren't sent to the server and are only used in the browser. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Initially, I planned to make a kind of 'universal URI-as-URL wrapper' that would allow any URI which could be successfully constructed into a URL with the WebAPI's &lt;code&gt;new URL()&lt;/code&gt; constructor to be used to create a redirect. However, I ran into a few issues. For one, this was an obvious security concern since potential bad actors could use the service to obfuscate malicious URLs/URIs. On a more practical note, Vercel's Edge Runtime seems to use different polyfills and a non-standard implementation of &lt;code&gt;Response.redirect()&lt;/code&gt;, which made it impossible to redirect to some (valid) URIs. This led me down the rabbit hole of URL technical specifications, state-machine based parsing and validation, and a firm reminder of how much of a miracle internet interoperability is. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>vercel</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Setting Up a (Free*) Collaborative Python Development Environment for a Small Team</title>
      <dc:creator>Kevin Cole</dc:creator>
      <pubDate>Mon, 16 Oct 2023 15:38:34 +0000</pubDate>
      <link>https://dev.to/kcole93/setting-up-a-free-collaborative-python-development-environment-for-a-small-team-bpo</link>
      <guid>https://dev.to/kcole93/setting-up-a-free-collaborative-python-development-environment-for-a-small-team-bpo</guid>
      <description>&lt;p&gt;Perhaps you've found yourself in this pickle: you're preparing to dig into a new coding project but this time, it won't just be &lt;em&gt;you&lt;/em&gt; doing the work.&lt;/p&gt;

&lt;p&gt;It's one thing to initiate a repository on your local machine and invite others to collaborate asynchronously through remote source management tools like Github, Gitlab or Codeberg; introducing the prospect of real-time collaboration and managing the dreaded "works on my machine" bumbles might warrant a bit more architectural thinking.&lt;/p&gt;

&lt;p&gt;I found myself in this situation last week while initiating a new research project that will require a small team of collaborators to work on a Python-heavy project exploring the potentials and pitfalls of leveraging LLMs to provide enhanced general orientation for asylum-seekers on their rights and the procedures which apply to them. It's always a struggle to tame that initial urge to jump into your IDE of choice and start coding, but it was clear that this project could benefit from some measures to ensure new team members could be brought onboard and enabled to contribute with minimal friction.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk through both our decision-making process and provide a guide on how to use GitPod, Github and Jupyter Notebook to set up a collaborative Python development environment.&lt;/p&gt;

&lt;p&gt;If you're just interested in the step-by-step guide, feel free to jump right there. And if you're really in a rush, you can fork &lt;a href="https://github.com/kcole93/python-cde-gitpod"&gt;this template repository&lt;/a&gt; as a basis for your own dev environment hosted by Gitpod.&lt;/p&gt;

&lt;h2&gt;
  
  
  To Containerize or Not to Containerize?
&lt;/h2&gt;

&lt;p&gt;If you're planning to work collaboratively on a Python-heavy project, one of the first determinations you'll need to make is whether or not you plan to use "containerization". To provide a simple definition relevant to our project planning, "containerization" is an approach to software development and deployment where code and/or services are run within a minimal virtualized runtime environment that is either hosted on a local machine or run remotely (aka, "in the cloud").&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;What's the point of "containerization"? In our use case, containerization's main benefit is to ensure that all contributing developers can access and run code in a single, standardized environment to reduce errors and incompatibilities caused by differing local runtime environments and configurations. It allows us to largely sidestep issues faced when contributors are working on different operating systems, have locally-installed libraries/packages which introduce incompatibilities, and the myriad other configuration permutations that naturally result from the ways in which we all use our own computers.&lt;/p&gt;

&lt;p&gt;And what about the drawbacks? In some cases, running your development environment within a container might introduce too much additional overhead. For example, running containers locally could require you to provide guidance on installing Docker on a plethora of individual computers—ack! Containerization can also lead to performance bottlenecks, especially for compute-heavy tasks like machine learning or 3D graphics rendering. Finally, taking full advantage of containerization frequently entails hosting these containerized environments remotely, and this can be quite costly!&lt;/p&gt;

&lt;p&gt;So, how to decide? Ultimately, you'll need to take a hard look at your organization's use case to make a final determination. In our case, the following considerations were a top priority:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Contributors should be able to access the development environment with just a fundamental understanding of common software development tools: VS Code or a similar IDE and git-based source control.&lt;sup id="fnref2"&gt;2&lt;/sup&gt; It's a priority to enable contributors to learn through their research in this project, so barriers to entry must be as low as possible.&lt;/li&gt;
&lt;li&gt;Contributors should not have to consider and manage project dependencies. Our priority is to enable direct contribution, rather than having to fiddle with configurations.&lt;/li&gt;
&lt;li&gt;The solution must enable secure handling of authentication data, allowing for the group's work to be shared openly while also permitting the use access-controlled resources (in our case, AWS Bedrock's LLM APIs).&lt;/li&gt;
&lt;li&gt;Considering the size of our team and the nature of our (non-profit) research, costs should be minimal (ideally, $0.00!)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Given this set of criteria, we opted to containerize our development environment but specifically chose to use a hosted "cloud-based development environment", rather than running our development container locally (for example, with Docker) or setting up our own (read, self-managed) remotely hosted container runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's a Cloud-based Development Environment?
&lt;/h2&gt;

&lt;p&gt;Cloud-based Development Environments (CDEs) are &lt;em&gt;remotely hosted&lt;/em&gt; runtimes used to enable one or many developers to work on software from different devices, and increasingly they're coupled with other tools in the developer's toolchain to provide one-click access to a "ready-to-code" state. You've probably come across some of the more popular service providers like GitPod, GitHub Codespaces, or Google's Cloud Workstations.&lt;/p&gt;

&lt;p&gt;So, what's the deal with CDEs? To bring things to a point, many popular CDE solutions exist in a grey zone between the three main cloud service business models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Software as a Service" (SaaS)&lt;/li&gt;
&lt;li&gt;"Platform as a Service" (PaaS)&lt;/li&gt;
&lt;li&gt;"Infrastructure as a Service" (IaaS)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As others have rightfully pointed out,&lt;sup id="fnref3"&gt;3&lt;/sup&gt; this means that using a (non self-hosted) CDE product makes you a current or potential future customer. At the same time, these products also deliver a valuable service, namely simplifying the overhead required to provision and manage your own remotely-run container instance.&lt;/p&gt;

&lt;p&gt;In the current environment of VC-funded "blitzscaling," small teams/projects (and in our case, particularly non-profit organizations) are generally able to skate by on the "generous free tiers" made possible by this phenomenon, though the same cautionary warnings ought still apply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generous Free Tiers are frequently a "loss lead" offering and as many the hobbyist has learned from experience (ahem, Heroku), they may one day simply cease to exist.
&lt;/li&gt;
&lt;li&gt;That related dread-word, "Vendor Lock In".
&lt;/li&gt;
&lt;li&gt;Overdependence on abstracted/productized solutions can lead to knowledge/practical experience gaps in teams.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Like most things in life, there are certainly a set of benefits and tradeoffs to be considered, so it's crucial to take a hard look at your project, team and organization's requirements, goals, resources and options while deciding on a path forward. Just don't get so bogged down in the weeds that you forget that you &lt;em&gt;can&lt;/em&gt; alter the direction of that path, even if doing so down the line might incur costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Solution: GitPod, GitHub &amp;amp; Jupyter Notebook
&lt;/h2&gt;

&lt;p&gt;After a bit of reflection on our primary goals, anti-goals, and operational constraints, we landed on the following set of tools for our Python-focused, research-oriented project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://gitpod.io/"&gt;GitPod&lt;/a&gt;: GitPod is an (open sourced!) CDE solution which strongly integrates with VS Code, provides straightforward configuration for the workspace's underlying container image, and provides some built-in support for handling access to the dev workspace and environmental variables.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/"&gt;GitHub&lt;/a&gt;: Our organization already uses GitHub to host and manage remote repositories, so this was a bit of a given.&lt;sup id="fnref4"&gt;4&lt;/sup&gt; Using a git-based source control system is a practical necessity in this type of collaborative project, allowing for &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://jupyter.org/"&gt;Jupyter Notebook&lt;/a&gt;: Because our project is research-focused, we decided to use Jupyter Notebook to maximize the accessibility and reproducibility of our work by leveraging the ability to directly document our approaches with Markdown in Jupyter Notebook's &lt;code&gt;.ipynb&lt;/code&gt; files.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You might &lt;em&gt;not&lt;/em&gt; want to take this approach if your project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires or greatly benefits from hardware acceleration;&lt;/li&gt;
&lt;li&gt;Requires you to run multiple concurrent and/or persistent services (databases, authentication servers, etc.);&lt;/li&gt;
&lt;li&gt;Needs to support full-time contributors: GitPod's free tier is &lt;a href="https://www.gitpod.io/pricing"&gt;&lt;strong&gt;capped at 50 hours of container up-time per month!&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting Up Our Workspace
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Initialize Your Repository&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The first step to getting your Gitpod workspace running is to &lt;a href="https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-new-repository"&gt;initialize a GitHub repository&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a GitPod Workspace (and optionally, a Gitpod Project)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
You can open your repository (or any repo your GitHub account has access to) in a Gitpod 'workspace' (i.e., ephemeral containerized runtime environment) by prepending &lt;code&gt;gitpod.io/#&lt;/code&gt; to your GitHub repo's URL. For example, you can open the forem project in a Gitpod Workspace by navigating to the following URL: &lt;code&gt;https://gitpod.io/#https://github.com/forem/forem&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
You'll be asked to select your preferred editor experience, be that VS Code for the Browser, VS Code Desktop or another supported desktop IDE, or via SSH. Regardless of your editing method of choice, your next step will be setting up Gitpod's configuration files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Adding Gitpod Dotfiles&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Most configuration for Gitpod Workspaces is handled by two Dotfiles which you'll want to place in the root directory of your project's repo: &lt;code&gt;.gitpod.yml&lt;/code&gt; and &lt;code&gt;.gitpod.Dockerfile&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;.gitpod.Dockerfile&lt;/strong&gt;: This (optional) file allows you more flexibility to use your own custom Dockerfile, rather than one of Gitpod's &lt;a href="https://hub.docker.com/u/gitpod/"&gt;official Docker images&lt;/a&gt;. For this set up, we'll create a &lt;code&gt;.gitpod.Dockerfile&lt;/code&gt; to ensure our container uses a consistent Python version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.gitpod.yml&lt;/strong&gt;: This file specifies the underlying Gitpod workspace image to use for your runtime environment and allows you to define commands to be run on workspace startup as well as what ports (if any) you'd like to expose.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's our &lt;code&gt;.gitpod.Dockerfile&lt;/code&gt;:&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="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gitpod/workspace-full&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; gitpod&lt;/span&gt;

&lt;span class="c"&gt;# Install and set global Python version to 3.11&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pyenv &lt;span class="nb"&gt;install &lt;/span&gt;3.11 &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pyenv global 3.11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Dockerfile provides Gitpod with instructions to spin up containers for our workspace using the default image, which comes pre-bundled with typical development tools. We chose to use the default image out of convenience, but the &lt;code&gt;gitpod/workspace-python&lt;/code&gt; image would have provided a lighter out-of-the-box footprint.&lt;/p&gt;

&lt;p&gt;It also instructs for that image to run two pyenv commands: one to install Python version 3.11, and one to set the global Python environment to v3.11. This is an important step for our project because some Langchain dependencies are currently incompatible with Python versions &amp;gt;= 12.0.&lt;/p&gt;

&lt;p&gt;We used the following configuration in our &lt;code&gt;.gitpod.yml&lt;/code&gt; file:&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.gitpod.Dockerfile&lt;/span&gt;

&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration does two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Instructs Gitpod to use our custom workspace image. &lt;/li&gt;
&lt;li&gt;&lt;p&gt;We instruct Gitpod to run the command &lt;code&gt;pip install -r requirements.txt&lt;/code&gt; when the workspace container starts up, ensuring that all necessary Python libraries are installed and available in the runtime environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Initialize &lt;code&gt;requirements.txt&lt;/code&gt; and Commit Changes&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Before we finish our work initializing Gitpod, we'll need to install some of our known requirements and importantly, persist our changes by committing to our GitHub repository.  You can use the terminal session connected to your Gitpod workspace to install Python libraries with pip. In our case, we'll install Jupyter Notebook:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;p&gt;Since we're working in an ephemeral workspace (container) and in a collaborative project, we'll need to be sure to save these changes by committing and pushing them to our GitHub repo. We'll use pip's &lt;code&gt;freeze&lt;/code&gt; command to save our currently-installed libraries to a &lt;code&gt;requirements.txt&lt;/code&gt; file in the root directory of our repo, so that all necessary libraries are installed each time our workspace image is recreated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"requirements.txt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command redirects the output of &lt;code&gt;pip freeze&lt;/code&gt; (a list of currently-installed libraries and their version) to the text file &lt;code&gt;requirements.txt&lt;/code&gt;, referenced in our &lt;code&gt;.gitpod.yml&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Now we're ready to persist all of these changes by committing and pushing to our GitHub repository. You can use the built-in VS Code (or your editor of choice) 'Source Control' panel, or alternatively use the terminal in your workspace:&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;# Stage changed files&lt;/span&gt;
git add &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="c"&gt;# Commit changes with a message&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Your commit message here"&lt;/span&gt;
&lt;span class="c"&gt;# Push these changes to the main branch of your remote repo&lt;/span&gt;
git push &lt;span class="nt"&gt;--set-upstream&lt;/span&gt; origin main 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, you've got a functional cloud-based development environment! To add contributors, you'll just need to ensure that they have appropriate access to your project's repository and that they've created a Gitpod account. As a next step, you might consider setting up branch protections in your GitHub repo to prevent unintentional commits to your &lt;code&gt;main&lt;/code&gt; branch by contributors, or further defining your development environment by specifying VS Code extensions to be pre-installed in your &lt;code&gt;.gitpod.yml&lt;/code&gt; Dotfile.&lt;/p&gt;

&lt;p&gt;If like our team you're planning to use Jupyter Notebook files, note that you'll get the best support using VS Code for Desktop, rather than the browser.&lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Concerns and Reflections
&lt;/h2&gt;

&lt;p&gt;Rolling out a CDE with Gitpod turned out to be pretty simple, but the "cautionary tales" aren't without virtue. With just 50 hours of container uptime a month, it's clear that Gitpod can't provide a completely cost-free solution for professional teams working fulltime, and there are strong arguments to be made for simplifying this collaborative project's architecture by using just a GitHub repo and a more robust environment management tool, like Conda.&lt;/p&gt;

&lt;p&gt;Working in a nonprofit and humanitarian organization brings with it a particular set of needs and goals which aren't necessarily always reflected in tech-first corporations, and it's important not to brush aside these priorities in favor of the architecture du jour.&lt;/p&gt;

&lt;p&gt;Some other potential drawbacks to the CDE approach include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network latency, which can be a major obstacle especially when working with contributors in areas with intermittent or weak internet connections;&lt;/li&gt;
&lt;li&gt;More limited options for managing larger file storage without introducing additional complexity;&lt;/li&gt;
&lt;li&gt;Limitations on data sovereignty given reliance on third-party hosting of project repositories.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That being said, using a CDE in the specific context of this initiative allows our team to lower barriers to meaningful contribution by focusing on commonly-taught tooling (VS Code, GitHub) and minimize time spent troubleshooting platform and machine-specific installation woes. It allows for day one exposure to the team's work for new contributors, while also allowing for the abstractions upholding the 'ready-to-code' environment to be elegantly surfaced and retired: contributors can transition to development on their local machine (with or without containerization) once they are confident in setting up their own environment by cloning the repository locally and installing dependencies.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;In practice, the term containerization encapsulates a broad approach in software engineering that may be used towards various ends, such as isolating the execution environment of potentially hazardous code from critical systems. IBM has a helpful overview of the topic at: &lt;a href="https://www.ibm.com/topics/containerization"&gt;https://www.ibm.com/topics/containerization&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;VS Code's &lt;a href="https://code.visualstudio.com/docs/sourcecontrol/overview"&gt;official documentation&lt;/a&gt; on using the Source Control panel is quite helpful as a teaching/learning resource! ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;I found Mike Nikle's blog post on the subject to offer a strong, if perhaps overly skeptical, view on the drawbacks of adopting CDE products: &lt;a href="https://www.mikenikles.com/blog/dev-environments-in-the-cloud-are-a-half-baked-solution"&gt;"Dev environments in the cloud are a half-baked solution"&lt;/a&gt;. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;It's worth noting here that GitPod also supports GitLab and Bitbucket. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;Per Gitpod's documentation: &lt;a href="https://www.gitpod.io/docs/introduction/languages/python#jupyter-notebooks-in-vs-code"&gt;https://www.gitpod.io/docs/introduction/languages/python#jupyter-notebooks-in-vs-code&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>python</category>
      <category>github</category>
      <category>cloud</category>
      <category>docker</category>
    </item>
  </channel>
</rss>
