<?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: Jerry Ng</title>
    <description>The latest articles on DEV Community by Jerry Ng (@jerrynsh).</description>
    <link>https://dev.to/jerrynsh</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%2F328573%2F4f7d2e37-25e5-417e-a642-914497b73a1f.JPG</url>
      <title>DEV Community: Jerry Ng</title>
      <link>https://dev.to/jerrynsh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jerrynsh"/>
    <language>en</language>
    <item>
      <title>A Look Back on 7 Years of Automating Stuff</title>
      <dc:creator>Jerry Ng</dc:creator>
      <pubDate>Mon, 04 Dec 2023 00:00:34 +0000</pubDate>
      <link>https://dev.to/jerrynsh/a-look-back-on-7-years-of-automating-stuff-50bl</link>
      <guid>https://dev.to/jerrynsh/a-look-back-on-7-years-of-automating-stuff-50bl</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iMDxQIWC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://images.unsplash.com/photo-1515162816999-a0c47dc192f7%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fHJlZmxlY3Rpb258ZW58MHx8fHwxNzAwMTg2Mzk2fDA%26ixlib%3Drb-4.0.3%26q%3D80%26w%3D2000" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iMDxQIWC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://images.unsplash.com/photo-1515162816999-a0c47dc192f7%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fHJlZmxlY3Rpb258ZW58MHx8fHwxNzAwMTg2Mzk2fDA%26ixlib%3Drb-4.0.3%26q%3D80%26w%3D2000" alt="A Look Back on 7 Years of Automating Stuff" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A little bit more than 7 years into my career, I thought it would be fun to pen down a summary post about my adventure in automating various tiny aspects of my life. Most if not all of the stuff here sprouted from my own problems and itches I needed to scratch.&lt;/p&gt;

&lt;p&gt;This post will probably read more like a personal diary of the minor nuances that I encountered and the sweet minutes/hours that I managed to snatch back through automation.&lt;/p&gt;

&lt;p&gt;On to the first one –&lt;/p&gt;

&lt;h2&gt;
  
  
  Six Percent: Automating ASNB Purchases
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;2018 – 2022&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://camo.githubusercontent.com/4e72c85701e6ebb6493f5b3465a850b4515f9695ccc2f940ef1ad32135e7ada7/68747470733a2f2f696d6775722e636f6d2f4959474d6f556f2e706e67" class="article-body-image-wrapper"&gt;&lt;img src="https://camo.githubusercontent.com/4e72c85701e6ebb6493f5b3465a850b4515f9695ccc2f940ef1ad32135e7ada7/68747470733a2f2f696d6775722e636f6d2f4959474d6f556f2e706e67" alt="A Look Back on 7 Years of Automating Stuff" width="449" height="253"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The startup window of the bot. This was the first thing that I've made that people actually use.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It all started with this Python script called "&lt;a href="https://github.com/ngshiheng/six-percent"&gt;six-percent&lt;/a&gt;", a seemingly dumb bot to click buttons for some investment.&lt;/p&gt;

&lt;p&gt;For context, ASNB a unit trust management company offers a &lt;a href="https://www.asnb.com.my/asnbv2_2funds_EN.php#hargatetap"&gt;fixed-price fund&lt;/a&gt; (i.e. it can never go up or down!) that promised a sweet &lt;strong&gt;6%&lt;/strong&gt; p.a. dividend back then. It’s virtually risk-free.&lt;/p&gt;

&lt;p&gt;Well, there was a catch. The limited funds pool meant that it was selling like hotcakes — you had to keep retrying to snag those units. So, what did I do? I wrote this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;p&gt;As this turns out to be a tiny Windows executable, it didn’t really cost me any money to host or anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free lunch, literally
&lt;/h3&gt;

&lt;p&gt;Beyond saving me countless mindless hours clicking like a headless chicken, I got a free thank-you meal out of it.&lt;/p&gt;

&lt;p&gt;Fast forward to today, and I'm no longer actively using or maintaining the project. Over the years, there have been a few minor bug fixes here and there, but it's essentially retired. I can't guarantee that it still works, but man, it saved me so many hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  What did I learn
&lt;/h3&gt;

&lt;p&gt;Today, I’ve become quite comfortable with any form of browser automation. If I can interact with it on the web, I can automate it. The script opened doors for tackling repetitive/mundane tasks, aiding in integration testing, etc.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Years later, I stumbled upon &lt;a href="https://github.com/go-rod/rod"&gt;go-rod&lt;/a&gt; which is significantly better in terms of ease of use and developer experience.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That aside, I did also learn other cool tricks like how to solve a CAPTCHA using &lt;a href="https://en.wikipedia.org/wiki/Optical_character_recognition"&gt;OCR&lt;/a&gt; and &lt;a href="https://jerrynsh.com/how-to-package-python-selenium-applications-with-pyinstaller/"&gt;how to package a Python app using PyInstaller&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Todoleet: Daily Leetcode Questions in Todoist
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;2021 – Present&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Back in early 2021, I started to solve the LeetCode Daily challenge as part of my morning routine. Quite frankly, it wasn’t exactly fun for me; it was necessary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--65rCe8bQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/11/image.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--65rCe8bQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/11/image.png" alt="A Look Back on 7 Years of Automating Stuff" width="800" height="277"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My personal Todolist. Nope, I'm not attempting this one.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;Even though I've ditched my morning LeetCode routine, Todoleet is still up and running today.&lt;/p&gt;

&lt;p&gt;Under the hood, it’s merely &lt;a href="https://github.com/ngshiheng/todoleet/blob/main/index.js"&gt;a simple JS script&lt;/a&gt; that talks to the undocumented LeetCode API and then creates a new to-do task using the Todoist API.&lt;/p&gt;

&lt;p&gt;If you're looking for the nitty-gritty, I've spilled the beans on &lt;a href="https://jerrynsh.com/how-i-sync-daily-leetcoding-challenge-to-todoist/"&gt;how I sync the Daily Leetcode Challenge to my Todoist&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;p&gt;$0. The free Cloudflare Worker tier has got me covered! This solution costs 1 request per day and I didn’t need to store anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Time saved
&lt;/h3&gt;

&lt;p&gt;I did manage to save a few clicks (seconds) every day. &lt;em&gt;They all add up, I guess?&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Any learnings?
&lt;/h3&gt;

&lt;p&gt;This was my introduction to Cloudflare Worker. It paved the road for all the &lt;a href="https://jerrynsh.com/tag/cloudflare-worker/"&gt;other projects&lt;/a&gt; I've tinkered with in my free time!&lt;/p&gt;

&lt;h3&gt;
  
  
  Future plans
&lt;/h3&gt;

&lt;p&gt;I did consider taking this to another level and listing it as a &lt;a href="https://todoist.com/integrations"&gt;Todoist integration&lt;/a&gt;. But, to be honest, I didn't care enough to make it happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Burplist: Sipping on Craft Beer Savings
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;2021 – Present&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Craft beers are delicious. There was just one hiccup — the price. Then I figured, wouldn’t it be great if I could have current and historical prices of all craft beers in Singapore, all in a place?&lt;/p&gt;

&lt;p&gt;Now, instead of wrestling with 10+ websites for the best deals, a quick search from my database would do the trick. This saves me time and sanity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--quw7qH7b--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/11/image-8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--quw7qH7b--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/11/image-8.png" alt="A Look Back on 7 Years of Automating Stuff" width="800" height="787"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;I was looking for some Dark Ale as I was writing this&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;Burplist is essentially a &lt;a href="https://github.com/ngshiheng/burplist/"&gt;web scraper built using Scrapy&lt;/a&gt;. It scours over 10 local online stores and e-commerce sites every morning (Singapore time), fetching craft beer prices and storing them in a Postgres database. As a result, I've amassed two years of historical craft beer price data in Singapore.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;p&gt;Other than the ~$10/year for the domain name, &lt;a href="https://jerrynsh.com/how-i-built-burplist-for-free/"&gt;running Burplist has always been free&lt;/a&gt;. After Heroku phased out of its free tier plan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The scraper Cron job was moved to &lt;a href="https://northflank.com/"&gt;Northflank&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The Postgres database is hosted on &lt;a href="https://railway.app/?referralCode=jerrynsh"&gt;Railway&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The website was moved to &lt;a href="https://www.koyeb.com/"&gt;Koyeb&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Free beer for that Christmas
&lt;/h3&gt;

&lt;p&gt;So, initially, I gave it a shot to make some money out of this. All I did was put it up for sale on Gumroad, but it didn't really catch on.&lt;/p&gt;

&lt;p&gt;I also tried reaching out to a few companies through cold emails to see if they'd be interested in partnering up with some affiliate links, but only one replied — well, it didn’t work out either.&lt;/p&gt;

&lt;p&gt;But hey, no big deal! It's all good because something awesome still came out of it! &lt;a href="https://www.thirsty.com.sg/"&gt;Thirsty&lt;/a&gt;, this local craft beer company, actually surprised me with this amazing package of craft beer for Christmas that year! I was over the moon.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Qk4HTEX4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/11/image-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Qk4HTEX4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/11/image-1.png" alt="A Look Back on 7 Years of Automating Stuff" width="576" height="531"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;I got a box of delicious craft beers for Christmas that year&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Any lesson learned?
&lt;/h3&gt;

&lt;p&gt;This was quite a big one. I’ve had so much fun and learned so much from making this project. Burplist is the &lt;em&gt;fanciest&lt;/em&gt; web scraper that I’ve built thus far. I’ve written down some of the learnings in a &lt;a href="https://jerrynsh.com/5-useful-tips-while-working-with-python-scrapy/"&gt;blog post&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;😂 Oh, and here's a quirky side effect: I’m now able to figure out whether a mega-sale/promotion is the real deal or just a clever markup with a discount disguise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Looking ahead
&lt;/h3&gt;

&lt;p&gt;Future plans? Maybe migrate from Postgres to SQLite. Then, update the daily job to push the SQLite file directly to GitHub and present the data through something like &lt;a href="https://phiresky.github.io/blog/2021/hosting-sqlite-databases-on-github-pages/"&gt;this nifty method&lt;/a&gt;. Expected result? One less webserver to babysit. If this ever happens, I’ll probably write about it somewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wraith: Automating Ghost Blog Backup
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;2022 – Present&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--yMEbSsO9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/11/image-4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yMEbSsO9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/11/image-4.png" alt="A Look Back on 7 Years of Automating Stuff" width="485" height="193"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A screenshot of my terminal emulator&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At the start of my blogging journey, I didn’t really think too much about what would happen if this $6/month Droplet crashed. I mean, I had all my blog entries in Notion, so I figured "Meh, it wouldn’t hurt to copy-paste ~10 of them back if anything bad happens".&lt;/p&gt;

&lt;p&gt;As time went on, the realization hit — losing all my data and whatnot now would really suck. So, the solution? Automate the backup. With the blog serving around 10k views monthly, any downtime or a prolonged &lt;code&gt;404&lt;/code&gt; or &lt;code&gt;500&lt;/code&gt; is something I'd rather avoid.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s underneath
&lt;/h3&gt;

&lt;p&gt;It’s a &lt;a href="https://github.com/ngshiheng/wraith/blob/ebefb095fa14ef4d5a5611a1eb3e6c4f70f559d3/backup.sh#L85"&gt;pretty simple Bash script&lt;/a&gt; that does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;ghost backup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mysqldump&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rclone&lt;/code&gt; to a remote drive (e.g. Dropbox, Google Drive, etc.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This Bash script gets a weekly run in a Cron job, and it's as easy as that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;p&gt;$0. I didn’t have to pay for Dropbox; the free tier fits my needs just fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Time saved
&lt;/h3&gt;

&lt;p&gt;I mean, the alternative would be manually SSH-ing into my Droplet, running backup steps one by one — a process taking roughly about 5 minutes or less. So, that's the weekly saving of ~5 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  What did I learn
&lt;/h3&gt;

&lt;p&gt;Picked up a few best practices for writing Bash script along the way. Besides that, I did learn about new tools like &lt;code&gt;expect&lt;/code&gt;, &lt;code&gt;rclone&lt;/code&gt;, and &lt;code&gt;pass&lt;/code&gt; (the &lt;a href="https://wiki.archlinux.org/title/Pass"&gt;Linux password manager&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Future plans
&lt;/h3&gt;

&lt;p&gt;Not a whole lot to be honest. Perhaps a backup restore script could be handy. Thought about moving passwords from plain text to using &lt;code&gt;pass&lt;/code&gt;. But, that means users dealing with GPG + Pass CLI setup — an extra hurdle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tournacat: Sync Esports Schedules to Google Calendar
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Started 2023&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I got tired of missing highly anticipated Dota 2 Esports matches, dealing with wonky time zones, and the mental gymnastics of remembering it all. Since Google Calendar is practically my second brain, I figured, why not sync upcoming matches straight into it?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--L25sMX5m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/12/image-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--L25sMX5m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/12/image-1.png" alt="A Look Back on 7 Years of Automating Stuff" width="800" height="343"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Month view of Google Calendar&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Turning it into a micro SaaS
&lt;/h3&gt;

&lt;p&gt;I started by sharing the Dota 2 calendar with friends. Then, a lightbulb moment — if it's handy for them, &lt;em&gt;maybe&lt;/em&gt; others would pay for it. And so, &lt;a href="https://jerrynsh.com/sync-dota-2-esports-matches-to-your-google-calendar/"&gt;D2GCal was born&lt;/a&gt;. Pay, and get a public link to the Google Calendar. Simple.&lt;/p&gt;

&lt;p&gt;But wait — people are picky about their calendars. Some find it “noisy”, and some want a customized experience. Enter &lt;a href="https://workspace.google.com/marketplace/app/tournacat/1041160187344"&gt;Tournacat&lt;/a&gt;, supporting 10+ Esports titles (not just limited to Dota 2!), giving users the power to own and customize their calendars.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;p&gt;~$10/year for now for the domain name.&lt;/p&gt;

&lt;p&gt;Firstly, the website (&lt;a href="https://tournacat.com/"&gt;tournacat.com&lt;/a&gt;) operates as a static site built using &lt;a href="https://gohugo.io/"&gt;Hugo&lt;/a&gt;. It's currently hosted using Cloudflare Pages which is free.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://workspace.google.com/marketplace/app/tournacat/1041160187344"&gt;Google Workspace add-on&lt;/a&gt; is built and runs on Google Apps Script (GAS) for free.&lt;/p&gt;

&lt;p&gt;Lastly, Tournacat has an API server running on Cloudflare Worker. It fetches the upcoming Esports events from a data source. Running on Cloudflare Worker is currently still within the free tier limit but it won't be so for long.&lt;/p&gt;

&lt;p&gt;The neat part? Tournacat doesn't store any user info. No names, no emails — nothing at all. This means that no database is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Revenue
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--v5oHXE-4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/11/image-10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--v5oHXE-4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/11/image-10.png" alt="A Look Back on 7 Years of Automating Stuff" width="489" height="480"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Lemon Squeezy payout page&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Today, Tournacat has about 90 users. In terms of paid subscriptions, it's made a tiny profit of &lt;em&gt;$40.23 in 11 months&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Honestly, I never expected to rake in big bucks anyway. The goal was to cover growing worker usage costs. Any surplus? Well, that's coffee money.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lessons learned
&lt;/h3&gt;

&lt;p&gt;The journey with Tournacat has been a blast. The realization that a handful of people want and use what you built yourself is an indescribable feeling.&lt;/p&gt;

&lt;p&gt;This tiny venture has taught me &lt;a href="https://jerrynsh.com/i-built-a-google-calendar-add-on-heres-what-i-learnt/"&gt;many valuable lessons&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Developing on the GAS platform&lt;/li&gt;
&lt;li&gt;Publishing of a Google Calendar add-on to Workspace&lt;/li&gt;
&lt;li&gt;Working with payments and subscriptions&lt;/li&gt;
&lt;li&gt;Crafting a micro SaaS&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Besides, the journey also introduced me to the mundane world of social media marketing, which, isn't my cup of tea. On the flip side, I had an amazing time speaking to people about their feedback (feel free to ask me anything)!&lt;/p&gt;

&lt;p&gt;Overall, I feel like the Esports landscape is shaped by a generation accustomed to abundant free entertainment and tools. Convincing people to spend money can be a tough sell.&lt;/p&gt;

&lt;h3&gt;
  
  
  Any future plans?
&lt;/h3&gt;

&lt;p&gt;There are plenty of tasks/tickets on the project board, but I'm taking it slow. The plan? I’ll just be working on minor improvements here and there unless specific user requests pop up.&lt;/p&gt;

&lt;p&gt;Being a solo developer has its perks — the turnaround time for new requests is pretty quick. I've been able to incorporate feedback almost immediately (as long as it's somewhat reasonable).&lt;/p&gt;

&lt;p&gt;Overall, the product feels pretty complete as it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  SGS Issuance Calendar: Automated T-Bill Tracking
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Started 2023&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Feeling the manual-checking fatigue for the Singapore MAS T-bill issuance calendar, I decided, "Why not automate this?" So, I crafted a schedule to run every month using Google Apps Script (GAS) and detailed the process in &lt;a href="https://jerrynsh.com/creating-a-mas-t-bill-calendar/"&gt;this blog post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2BdY3_NS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/11/image-6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2BdY3_NS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/11/image-6.png" alt="A Look Back on 7 Years of Automating Stuff" width="800" height="476"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;T-bill announcement and auction dates are on my calendar!&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How it Works
&lt;/h3&gt;

&lt;p&gt;Under the hood, it's a small &lt;a href="https://github.com/ngshiheng/sgs-issuance-calendar"&gt;GAS project written in TypeScript&lt;/a&gt;. The script gets triggered every month; pings the MAS API, and then creates important dates (e.g. announcement/auction date) as Google Calendar events. Simple as that!&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;p&gt;$0. The best part about using GAS is that I don’t need to be bothered by the underlying infrastructure hassle. No fretting over scaling, upgrades, backups, availability — none of that. It just does its thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Some lessons learned
&lt;/h3&gt;

&lt;p&gt;This project brought some learnings to the table. I am now able to build GAS projects in TypeScript and delved into the world of writing unit tests for GAS. This newfound skillset later found a home in Tournacat's add-on UI codebase, which is also written in GAS.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's next?
&lt;/h3&gt;

&lt;p&gt;I've added support for SGS and SSB calendars as well! For now, no concrete plans unless user feedback or GitHub issues come knocking. Two months in, and it's been serving me well.&lt;/p&gt;

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

&lt;p&gt;The journey has been nothing short of fun. My approach to automating/building usually unfolds like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify my own pain point or problem (note it down immediately!); however small it may be. Look for quick wins!&lt;/li&gt;
&lt;li&gt;Prefer leveraging what's already out there. Check if there's something in the market for it, like Zapier or IFTTT&lt;/li&gt;
&lt;li&gt;If not, then roll up my sleeves and code/build&lt;/li&gt;
&lt;li&gt;See if I can generalize the solution or approach&lt;/li&gt;
&lt;li&gt;Write down the process somewhere&lt;/li&gt;
&lt;li&gt;Rinse and repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  “Aren’t some of these side projects?"
&lt;/h3&gt;

&lt;p&gt;Well, I guess. I just think the term feels so worn out at this point. It's like, they're more about automating some parts of my life. Sure, one of them is making coffee money monthly, a bit laughable. The whole idea of “hustling” just doesn't quite vibe with me. I've realized I just want to do things &lt;a href="https://justforfunnoreally.dev/"&gt;just for fun. No, really&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks for sticking around! 🍻 Here's to more automation to come!&lt;/p&gt;

</description>
      <category>tinyproject</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built a Google Calendar Add-on. Here's What I Learnt</title>
      <dc:creator>Jerry Ng</dc:creator>
      <pubDate>Tue, 02 May 2023 00:00:21 +0000</pubDate>
      <link>https://dev.to/jerrynsh/i-built-a-google-calendar-add-on-heres-what-i-learnt-8oi</link>
      <guid>https://dev.to/jerrynsh/i-built-a-google-calendar-add-on-heres-what-i-learnt-8oi</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vvcE_tgj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/04/home_1280_720.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vvcE_tgj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/04/home_1280_720.png" alt="I Built a Google Calendar Add-on. Here's What I Learnt" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I made &lt;a href="https://tournacat.com/"&gt;Tournacat&lt;/a&gt;, an add-on that syncs upcoming Esports schedules of tournaments to Google Calendar. Two months later, Tournacat has garnered 4 paid users and is slowly growing.&lt;/p&gt;

&lt;p&gt;In this post, I will share my experience building Tournacat using various technologies including Cloudflare Worker, Hugo, and Google Apps Script.&lt;/p&gt;

&lt;p&gt;Along the way, I encountered some interesting challenges and learned valuable lessons. Specifically, I want to share my anecdotal experience with issues I encountered while developing the Tournacat add-on using Google Apps Script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Did I Make Tournacat
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ji3WGhSE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/04/month_1280_720.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ji3WGhSE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/04/month_1280_720.png" alt="I Built a Google Calendar Add-on. Here's What I Learnt" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A "Month" view of upcoming Esports schedules in Google Calendar.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Tournacat &lt;a href="https://jerrynsh.com/sync-dota-2-esports-matches-to-your-google-calendar/"&gt;started as an idea from D2GCal&lt;/a&gt;, a link to a Google Calendar featuring upcoming &lt;a href="https://www.dota2.com/"&gt;Dota 2&lt;/a&gt; tournaments available for purchase on &lt;a href="https://jerrynsh.gumroad.com/l/d2gcal"&gt;Gumroad&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I built Tournacat out of the frustration of missing out on big Dota 2 matches. I was tired of constantly scouring the internet to find out when my favorite matches were happening.&lt;/p&gt;

&lt;p&gt;As an avid Esports fan, I knew there had to be a better way (&lt;em&gt;cliché I know&lt;/em&gt;). So, I started building Tournacat for my own use.&lt;/p&gt;

&lt;p&gt;Today, Tournacat has grown to support major Esports titles including &lt;em&gt;Counter-Strike&lt;/em&gt;, &lt;em&gt;Dota 2&lt;/em&gt;, &lt;em&gt;LoL&lt;/em&gt;, &lt;em&gt;Valorant&lt;/em&gt;, &lt;em&gt;Overwatch&lt;/em&gt;, &lt;a href="https://tournacat.com/faqs/#what-games-and-esports-titles-are-covered"&gt;and more&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Google Calendar
&lt;/h2&gt;

&lt;p&gt;I use Google Calendar extensively in my daily life, from scheduling meetings to keeping track of personal events. So, having an Esports schedule alongside everything in one place is a huge plus.&lt;/p&gt;

&lt;p&gt;With Tournacat, you can just &lt;a href="https://workspace.google.com/marketplace/app/tournacat/1041160187344"&gt;install the add-on to your Google Calendar&lt;/a&gt; and let it work its magic. It's a seamless solution that integrates perfectly with Google Calendar.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DuB1ViWs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/04/schedule_1280_720.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DuB1ViWs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/04/schedule_1280_720.png" alt="I Built a Google Calendar Add-on. Here's What I Learnt" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A "Schedule" view of upcoming Esports schedules in Google Calendar.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Existing Solutions (Calendars)
&lt;/h3&gt;

&lt;p&gt;I've been on the lookout for a solution like Tournacat for years. I've tried all sorts of public Esports calendars in the past, but they always fell short in one way or another.&lt;/p&gt;

&lt;p&gt;Either the author of the calendar stopped updating it, or it was just an unreliable source that was prone to human error. It got to the point where I gave up hope of ever finding a calendar that actually worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;Tournacat is built using a variety of tools and technologies. The tech stack includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend — &lt;a href="https://gohugo.io/"&gt;Hugo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Backend — &lt;a href="https://workers.cloudflare.com/"&gt;Cloudflare Worker&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Add-on — &lt;a href="https://developers.google.com/apps-script"&gt;Google Apps Script&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hugo
&lt;/h3&gt;

&lt;p&gt;Hugo is a &lt;a href="https://www.gatsbyjs.com/docs/glossary/static-site-generator/#what-is-a-static-site-generator"&gt;static site generator&lt;/a&gt; that I used to create the &lt;a href="https://tournacat.com/"&gt;Tournacat website&lt;/a&gt;. I decided to use Hugo for a couple of reasons.&lt;/p&gt;

&lt;p&gt;First, I'm not particularly skilled in front-end development, so I wanted to use a static site generator that would allow me to use pre-built themes without having to write much front-end code myself. Hugo fit the bill perfectly in this regard.&lt;/p&gt;

&lt;p&gt;Second, Hugo is known for its speed and ease of use. I wanted to be able to iterate quickly and not have to spend a lot of time fussing with my site's performance or configuration. Hugo's documentation and tooling made it easy to get up and running quickly. With that, I was able to focus on building out the content for Tournacat’s website rather than worrying about technical details.&lt;/p&gt;

&lt;p&gt;I then hosted the website on &lt;a href="https://pages.cloudflare.com/"&gt;Cloudflare Pages&lt;/a&gt;. Using Cloudflare Pages allowed me to keep costs down, as I didn't need to pay for expensive hosting services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloudflare Worker
&lt;/h3&gt;

&lt;p&gt;For the backend, I decided to go with Cloudflare Worker. Cloudflare Worker is a &lt;a href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"&gt;serverless computing&lt;/a&gt; platform that allows you to run JavaScript code &lt;a href="https://www.cloudflare.com/learning/serverless/glossary/what-is-edge-computing/"&gt;on the edge&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To make my life easier, I decided to build the backend server Tournacat on top of a framework — Worktop, a lightweight Cloudflare Worker web framework that simplifies the development of APIs using TypeScript.&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;GitHub - lukeed/worktop: The next generation web framework for Cloudflare Workers&lt;/p&gt;

&lt;p&gt;The next generation web framework for Cloudflare Workers - GitHub - lukeed/worktop: The next generation web framework for Cloudflare Workers&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vxfVixPa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://github.com/fluidicon.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vxfVixPa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://github.com/fluidicon.png" alt="I Built a Google Calendar Add-on. Here's What I Learnt" width="512" height="512"&gt;&lt;/a&gt;GitHublukeed&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---DMad1bE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://opengraph.githubassets.com/e44527f3b710cb8af5863f9ce7f184d99eadd12dbf236788cf1669c4649e2a2d/lukeed/worktop" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---DMad1bE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://opengraph.githubassets.com/e44527f3b710cb8af5863f9ce7f184d99eadd12dbf236788cf1669c4649e2a2d/lukeed/worktop" alt="I Built a Google Calendar Add-on. Here's What I Learnt" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
](&lt;a href="https://github.com/lukeed/worktop"&gt;https://github.com/lukeed/worktop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Given the opportunity to start over, I would consider using &lt;a href="https://hono.dev/"&gt;Hono&lt;/a&gt; instead, as it appears to be more actively maintained &lt;a href="https://github.com/honojs/hono"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My decision to choose Cloudflare Worker was based on my comfort level with it, &lt;a href="https://jerrynsh.com/tag/cloudflare-worker/"&gt;having done several previous projects&lt;/a&gt; with it. The developer experience was nothing short of amazing, and deployment was easy. Furthermore, it was &lt;a href="https://developers.cloudflare.com/workers/platform/limits/#worker-limits"&gt;affordable with a generous free tier&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While the performance was impressive, it was not the main reason why I picked it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google Apps Script
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.google.com/script/start/"&gt;Google Apps Script&lt;/a&gt; is the core technology behind the &lt;a href="https://workspace.google.com/marketplace/app/tournacat/1041160187344"&gt;Tournacat add-on&lt;/a&gt;. It is a scripting language based on JavaScript that allows you to extend and automate Google Workspace products like Google Calendar, Sheets, and Drive.&lt;/p&gt;

&lt;p&gt;While building Google Workspace add-ons is possible with &lt;a href="https://developers.google.com/workspace/add-ons/guides/alternate-runtimes"&gt;other coding languages (runtimes)&lt;/a&gt;, I decided to go for Google Apps Script anyway due to convenience. Frankly, I never thought that I’d be impressed at how simple it is to build a workspace add-on!&lt;/p&gt;

&lt;p&gt;Do keep in mind that it does come with &lt;a href="https://developers.google.com/apps-script/guides/services/quotas"&gt;some limitations and quotas&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Navigating the Bumps
&lt;/h2&gt;

&lt;p&gt;Creating a Google Workspace add-on is relatively straightforward. The &lt;a href="https://developers.google.com/apps-script/add-ons/cats-quickstart"&gt;add-on tutorial&lt;/a&gt; provided by Google Workspace is pretty easy to follow.&lt;/p&gt;

&lt;p&gt;In short, It took me a day to figure out most things and about a week to get the add-on approved and published on &lt;a href="https://workspace.google.com/marketplace"&gt;Google Workspace Marketplace&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advantages
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;One of the pros of using Google Apps Script is that it literally cost nothing to run.&lt;/li&gt;
&lt;li&gt;Since it uses JavaScript, it's easy for most developers to pick up and start working with.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Challenges
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Although Google claims that &lt;a href="https://developers.google.com/apps-script/guides/v8-runtime"&gt;Apps Script is now supported by the V8 runtime&lt;/a&gt;, it doesn't &lt;em&gt;actually&lt;/em&gt; support Promises or async-await (&lt;a href="https://issuetracker.google.com/issues/149937257"&gt;Issue Tracker&lt;/a&gt;). Consequently, you may need to resort to &lt;a href="https://stackoverflow.com/a/59750451"&gt;using workarounds like triggers&lt;/a&gt; (Stackoverflow references: &lt;a href="https://stackoverflow.com/questions/31241396/is-google-apps-script-synchronous"&gt;1,&lt;/a&gt; &lt;a href="https://stackoverflow.com/questions/61578224/does-google-apps-script-v8-engine-support-promise"&gt;2&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Pushing changes to the Workspace add-on is rather unorthodox. I noticed this when one of my users couldn’t get their time-based triggers to work properly after a new deployment. Turns out the “correct” way to update a workspace add-on is to &lt;strong&gt;edit&lt;/strong&gt; a versioned deployment instead of creating a new one (&lt;a href="https://stackoverflow.com/questions/69294697/workspace-add-on-with-installable-calendar-event-trigger-stops-working-with-new/69300465#69300465"&gt;Stackoverflow reference&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jtSyqmfM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/04/image.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jtSyqmfM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2023/04/image.png" alt="I Built a Google Calendar Add-on. Here's What I Learnt" width="800" height="260"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Edit a versioned deployment with "New version" to update a workspace add-on&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;It has to respect the &lt;a href="https://developers.google.com/apps-script/guides/services/quotas"&gt;limits set by Google&lt;/a&gt;. However, it wasn't documented if the limits were shared across all add-on users cumulatively (&lt;em&gt;hint: it is not&lt;/em&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lastly, I came across an authentication-related issue (Stackoverflow references: &lt;a href="https://stackoverflow.com/questions/65124449/trigger-error-were-sorry-a-server-error-occurred-while-reading-from-storage"&gt;1,&lt;/a&gt; &lt;a href="https://stackoverflow.com/questions/60442915/container-bound-script-getting-permission-errors-trying-to-run-functions-with-go"&gt;2&lt;/a&gt;) when attempting to use Google Apps Script as a webhook to allow my users to automatically activate their subscriptions after payment. Some answers suggested &lt;a href="https://stackoverflow.com/a/65124855"&gt;temporarily disabling the V8 runtime&lt;/a&gt; to fix this.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Despite these hiccups, Google Apps Script is a great platform to work with. It gets 90% of the job done very effectively and the last 10% is the challenges that I mentioned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Some Lessons Learned
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use what makes you the most productive.&lt;/strong&gt; Don’t worry about picking the perfect tech stack that handles scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Users couldn’t care less about your code.&lt;/strong&gt; Nobody except you cares about the fancy frameworks, design patterns, or architecture used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep your deployment simple.&lt;/strong&gt; This allows for fast iterations. While I could have gone all-in with AWS and set up a sophisticated Terraform setup, I realized that I would have taken more time to ship if I were to go this route.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;I have always had the itch to build a micro SaaS. So, I decided to build one realizing that it wouldn’t make me rich or change the world. I simply enjoy building stuff that people want to use and money is the ultimate validation for that.&lt;/p&gt;

&lt;p&gt;However, building a SaaS was not exactly what I thought it would be — keeping my head down and coding away. I needed to understand my users and their needs and learn how to market my product effectively.&lt;/p&gt;

&lt;p&gt;I must admit, marketing is not something I truly enjoy. But, I realized that it's a necessary job that comes with building a product.&lt;/p&gt;

&lt;p&gt;Overall, building Tournacat was a rewarding experience. Seeing Tournacat slowly grow and gain traction has been an exciting journey. I'm eager to continue on this journey and I look forward to what the future holds.&lt;/p&gt;

</description>
      <category>tinyproject</category>
      <category>googleappsscript</category>
    </item>
    <item>
      <title>Python Decorators: Explained in What Why When</title>
      <dc:creator>Jerry Ng</dc:creator>
      <pubDate>Sun, 12 Feb 2023 16:00:00 +0000</pubDate>
      <link>https://dev.to/jerrynsh/python-decorators-explained-in-what-why-when-2ok</link>
      <guid>https://dev.to/jerrynsh/python-decorators-explained-in-what-why-when-2ok</guid>
      <description>&lt;p&gt;Decorators are incredibly powerful in Python. They are useful for creating modular and reusable code. Plus, they're pretty cool once you get the hang of them!&lt;/p&gt;

&lt;p&gt;In this blog post, we'll take a look at what decorators are, why you should use them, and when you should and shouldn't use them.&lt;/p&gt;

&lt;p&gt;As always, I’ll try to explain everything in layman's terms. My goal is to get you comfortable with decorators.&lt;/p&gt;

&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  Think of Python decorators as little "wrappers" that you can place around a function (or another object) to modify its behavior in some way.&lt;/li&gt;
&lt;li&gt;  You would typically use decorators for logging, authentication, or to make a function run faster by caching its result.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What are decorators
&lt;/h2&gt;

&lt;p&gt;Let’s start with a simple explanation of what decorators are in Python.&lt;/p&gt;

&lt;p&gt;A decorator is really just a function that takes in a function as an argument. It then returns a modified version of that function.&lt;/p&gt;

&lt;p&gt;Here’s a classic example:&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;my_decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&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;wrapper&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;Before the function is called.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;func&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;After the function is called.&lt;/span&gt;&lt;span class="sh"&gt;"&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;wrapper&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  In this example, the &lt;code&gt;my_decorator&lt;/code&gt; function takes a function as an argument (which we'll call &lt;code&gt;func&lt;/code&gt;) and defines a new inner function called &lt;code&gt;wrapper&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  The &lt;code&gt;wrapper&lt;/code&gt; function prints a message before and after calling the original &lt;code&gt;func&lt;/code&gt; function.&lt;/li&gt;
&lt;li&gt;  The &lt;code&gt;my_decorator&lt;/code&gt; function then returns the &lt;code&gt;wrapper&lt;/code&gt; function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To use a decorator, we would apply it to a function using the &lt;code&gt;@&lt;/code&gt; symbol, e.g. &lt;code&gt;@my_decorator&lt;/code&gt;. Here’s an example:&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;my_decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@my_decorator&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;greet_ben&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;Hello, Ben!&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;If we were to call &lt;code&gt;greet_ben&lt;/code&gt;, the output would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Before the &lt;span class="k"&gt;function &lt;/span&gt;is called.
Hello, Ben!
After the &lt;span class="k"&gt;function &lt;/span&gt;is called.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See how we effectively modified &lt;code&gt;greet_ben&lt;/code&gt; to print additional messages without changing the code of the original function itself?&lt;/p&gt;

&lt;h3&gt;
  
  
  Syntactic sugar
&lt;/h3&gt;

&lt;p&gt;You see, the decorator syntax &lt;code&gt;@my_decorator&lt;/code&gt; is merely syntactic sugar. The 2 following function definitions are semantically equivalent:&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;my_decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# Function definition 1
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;greet_ben&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;greet_ben&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;my_decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;greet_ben&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Function definition 2
&lt;/span&gt;&lt;span class="nd"&gt;@my_decorator&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;greet_ben&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interesting, right?&lt;/p&gt;

&lt;h3&gt;
  
  
  Python decorator template
&lt;/h3&gt;

&lt;p&gt;Now, hold your thought for a second, that was not how we would typically write a decorator function in Python — there are better ways.&lt;/p&gt;

&lt;p&gt;Instead, &lt;a href="https://gist.github.com/jaantollander/48480cfabbe98a7efe4c9848a5b55e6a" rel="noopener noreferrer"&gt;check out this template&lt;/a&gt; later to learn how to write a generic decorator function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use decorators
&lt;/h2&gt;

&lt;p&gt;Understanding the “why” is kind of like adding the icing on the cake — it just makes the whole thing a lot sweeter. So why use decorators at all?&lt;/p&gt;

&lt;p&gt;Well, decorators provide a flexible and convenient way to add extra functionality to existing functions, making them more &lt;em&gt;reusable&lt;/em&gt; and &lt;em&gt;modular&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is useful when you want to add a feature to a function that is used in multiple places in your code, but you don't want to modify the original function itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avoid cluttering
&lt;/h3&gt;

&lt;p&gt;For example, you might want to add logging or error handling to a function. But, you don't want to clutter the original function with that extra code.&lt;/p&gt;

&lt;p&gt;By using a decorator, you can add extra functionality without changing the original function, making your code cleaner and more organized.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use decorators
&lt;/h2&gt;

&lt;p&gt;Now that we know why we should use decorators, let's talk about when to use them.&lt;/p&gt;

&lt;p&gt;Generally speaking, Python decorators are most useful when you want to add extra functionality to a function without modifying its code. This might include things like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Logging&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/library/functools.html#functools.cache" rel="noopener noreferrer"&gt;Caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Authentication (&lt;a href="https://circleci.com/blog/authentication-decorators-flask/" rel="noopener noreferrer"&gt;see example in Flask&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Timing a function execution (&lt;a href="https://gist.github.com/Integralist/77d73b2380e4645b564c28c53fae71fb" rel="noopener noreferrer"&gt;see an advanced example&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A caching example
&lt;/h3&gt;

&lt;p&gt;Here's an example of how you might implement a simple cache for a Fibonacci function:&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="n"&gt;cache&lt;/span&gt; &lt;span class="o"&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;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&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;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cache&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;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&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;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;return&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# 354224848179261915075
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the caching code lives inside the &lt;code&gt;fib&lt;/code&gt; function itself.&lt;/p&gt;

&lt;p&gt;However, with decorators, we can simply define a caching decorator and use it to add caching functionality to our Fibonacci function:&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="n"&gt;cache&lt;/span&gt; &lt;span class="o"&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;cache_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&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;wrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&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;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cache&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;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wrapper&lt;/span&gt;

&lt;span class="nd"&gt;@cache_results&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&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;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;return&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&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="nf"&gt;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# 354224848179261915075
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  Here, we've implemented our own simple cache using a dictionary.&lt;/li&gt;
&lt;li&gt;  We've also defined our own decorator function &lt;code&gt;cache_results&lt;/code&gt; that takes a function as an argument and returns a new function that wraps the original function.&lt;/li&gt;
&lt;li&gt;  This inner function checks the cache to see if a result has already been calculated, and if so, it returns the cached result.&lt;/li&gt;
&lt;li&gt;  Otherwise, it calculates the result and adds it to the cache before returning it.&lt;/li&gt;
&lt;li&gt;  This way, we can apply the &lt;code&gt;cache_results&lt;/code&gt; decorator to any function we want to add caching for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While both examples work, the latter allows us to easily add caching to other functions without having to write the caching code inside each function.&lt;/p&gt;

&lt;p&gt;This makes the caching code in our example more reusable and modular!&lt;/p&gt;

&lt;h3&gt;
  
  
  A better example with built-in modules!
&lt;/h3&gt;

&lt;p&gt;Instead of reinventing the wheel, we can use the &lt;code&gt;lru_cache&lt;/code&gt; decorator from the &lt;code&gt;functools&lt;/code&gt; module to add caching to your &lt;code&gt;fib&lt;/code&gt; function:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lru_cache&lt;/span&gt;

&lt;span class="nd"&gt;@lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&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;def&lt;/span&gt; &lt;span class="nf"&gt;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&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;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;return&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&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="nf"&gt;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# 354224848179261915075
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  Using &lt;code&gt;lru_cache&lt;/code&gt; decorator is really easy – all you have to do is add the decorator to your function and it automatically handles the caching for you.&lt;/li&gt;
&lt;li&gt;  So, using &lt;code&gt;lru_cache&lt;/code&gt; can be a great way to optimize your code without a lot of extra effort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again, this is just one example of how you can use decorators to make your code more efficient and elegant.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use decorators
&lt;/h2&gt;

&lt;p&gt;So, when should you avoid using decorators?&lt;/p&gt;

&lt;p&gt;Well, decorators are great for keeping your code clean and easy to read, but they can make your code harder to understand if overused or used incorrectly.&lt;/p&gt;

&lt;p&gt;In general, it's best to avoid using decorators if they make your code more complex than it needs to be, or if you're not sure how they work.&lt;/p&gt;

&lt;p&gt;For instance, if you're working on a very small project and you don't need to keep track of how many times your functions are called, then you probably don't need to use a decorator.&lt;/p&gt;

&lt;p&gt;In some cases, you may find that it might be more appropriate to modify the code of the original function directly, rather than using a decorator.&lt;/p&gt;

&lt;p&gt;It's always a good idea to keep things simple and easy to understand!&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In short, Python decorators are like little "wrappers" that you can place around a function (or another object) to modify its behavior in some way.&lt;/p&gt;

&lt;p&gt;Decorators are useful for creating modular and reusable code in Python.&lt;/p&gt;

&lt;p&gt;You can define a decorator once and then apply it to any number of functions. This allows you to easily add the same extra functionality to multiple functions without repeating yourself.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published at &lt;a href="https://jerrynsh.com/python-decorators-explained-in-what-why-when/" rel="noopener noreferrer"&gt;jerrynsh.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>discuss</category>
    </item>
    <item>
      <title>Say Goodbye to Heroku Free Tier: Here Are 4 Alternatives</title>
      <dc:creator>Jerry Ng</dc:creator>
      <pubDate>Sun, 12 Feb 2023 14:12:12 +0000</pubDate>
      <link>https://dev.to/jerrynsh/say-goodbye-to-heroku-free-tier-here-are-4-alternatives-2lpi</link>
      <guid>https://dev.to/jerrynsh/say-goodbye-to-heroku-free-tier-here-are-4-alternatives-2lpi</guid>
      <description>&lt;p&gt;28 November 2022 was a sad day for developers. If you haven't heard, Salesforce (Heroku’s parent organization) has phased out its free tier plan on this date.&lt;/p&gt;

&lt;p&gt;For many years, Heroku has been the de facto standard platform as a service (PaaS). So many students and developers deployed their first web application on Heroku. Anecdotally, Heroku has been pivotal for my career.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TL;DR, if you’re looking for Heroku free tier alternatives, check out &lt;a href="https://free-for.dev/#/?id=paas" rel="noopener noreferrer"&gt;free-for.dev/#/?id=paas&lt;/a&gt;. I migrated my Cron jobs to &lt;a href="https://northflank.com/pricing" rel="noopener noreferrer"&gt;Northflank&lt;/a&gt; and Heroku Dynos to Koyeb.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A lot has happened
&lt;/h2&gt;

&lt;p&gt;For context, a lot has happened in the year 2022 for Heroku. The two most notable events are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On April 2022, Heroku had a security breach (&lt;a href="https://status.heroku.com/incidents/2413" rel="noopener noreferrer"&gt;incident&lt;/a&gt;) where CI and Review App secrets were compromised. GitHub Actions integrations on Heroku were down for a couple of months. Comms from Heroku were objectively bad. I experienced this firsthand when I had to resort to using a &lt;a href="https://github.com/marketplace/actions/deploy-to-heroku" rel="noopener noreferrer"&gt;GitHub CI Action&lt;/a&gt; for my app deployments.&lt;/li&gt;
&lt;li&gt;On August 2022, &lt;a href="https://blog.heroku.com/next-chapter" rel="noopener noreferrer"&gt;Heroku announced the removal of their free product plans&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you have been keeping an eye on the Internet, you would see people expressing concerns about how &lt;a href="https://news.ycombinator.com/item?id=31390506" rel="noopener noreferrer"&gt;Heroku is losing its magic&lt;/a&gt;. Ever since the security breach incident, I see more talks about Heroku alternatives taking place.&lt;/p&gt;

&lt;p&gt;Though things started to look bad, whatever Heroku had to offer was still great and I decided to stick to it. Until now, that is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Heroku alternatives
&lt;/h2&gt;

&lt;p&gt;For the past couple of weeks, I have been moving my demos and &lt;a href="https://jerrynsh.com/tag/tiny-project/" rel="noopener noreferrer"&gt;tiny projects&lt;/a&gt; out of Heroku.&lt;/p&gt;

&lt;p&gt;If you’re still looking for Heroku free tier alternatives, check out &lt;a href="https://free-for.dev/#/?id=paas" rel="noopener noreferrer"&gt;free-for.dev&lt;/a&gt; (&lt;a href="https://github.com/ripienaar/free-for-dev" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;While there are a bunch of blog posts and recommendations scattered on the Internet now, free-for.dev provides the best coverage. The list comes with a brief description of what each Heroku alternative does.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I was looking for
&lt;/h3&gt;

&lt;p&gt;Do note that my use case is mostly for small-scale personal projects. So, whatever I have written may not be ideal for your use case.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Equivalent &lt;strong&gt;free tiers&lt;/strong&gt; in terms of CPU and RAM. (&lt;em&gt;spoiler: none of them were as good&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;Support for Cron jobs out of the box. Ideally, no weird workaround is required&lt;/li&gt;
&lt;li&gt;Custom domain&lt;/li&gt;
&lt;li&gt;GitHub integration&lt;/li&gt;
&lt;li&gt;Support for basic logging and monitoring (i.e. CPU, memory, disk, network metrics)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The bandwidth cost and supported region of each PaaS are not my primary concerns.&lt;/p&gt;

&lt;h3&gt;
  
  
  My picks
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Heroku Postgres&lt;/em&gt; — since May 2022, &lt;a href="https://jerrynsh.com/saying-goodbye-to-heroku-postgres/" rel="noopener noreferrer"&gt;I have migrated from Heroku Postgres to Railway&lt;/a&gt;. So far, I have no complaints about it. Another alternative that I was looking at was &lt;a href="https://planetscale.com/" rel="noopener noreferrer"&gt;PlanetScale&lt;/a&gt;. However, I didn’t go for it because it isn’t Postgres-compatible.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Heroku Dynos apps&lt;/em&gt; — &lt;a href="https://burplist.me/" rel="noopener noreferrer"&gt;Burplist&lt;/a&gt; was migrated to &lt;a href="https://www.koyeb.com/tutorials/migrate-from-heroku" rel="noopener noreferrer"&gt;Koyeb&lt;/a&gt;. Having tried out other notable Heroku alternatives like &lt;a href="https://fly.io/docs/rails/getting-started/migrate-from-heroku/" rel="noopener noreferrer"&gt;Fly.io&lt;/a&gt;, &lt;a href="https://northflank.com/docs/v1/application/migrate-from-heroku" rel="noopener noreferrer"&gt;Northflank&lt;/a&gt;, and &lt;a href="https://railway.app/heroku" rel="noopener noreferrer"&gt;Railway&lt;/a&gt;, I can safely say that the migration from Heroku to Koyeb required the least amount of effort and kinks. It just works in my case.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Heroku Scheduler&lt;/em&gt; — for my Cron jobs, I opted for Northflank. Their support for Cron jobs is by far the best. Pricing aside, the developer experience is much better than Heroku Scheduler.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Heroku Add-ons&lt;/em&gt; — not applicable to my case. I was relying on Heroku Add-ons for monitoring and logging. Thankfully most of the modern PaaS today support that out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Some thoughts
&lt;/h3&gt;

&lt;p&gt;Woefully, most Heroku alternatives’ free tiers aren’t as good as Heroku. For example, &lt;em&gt;most&lt;/em&gt; of the free compute instances provided are 1 shared CPU and 256 MB RAM (Heroku Free Dynos started at 512 MB RAM). Not considering the limited number of apps allowed yet.&lt;/p&gt;

&lt;p&gt;To my surprise, most of these PaaS still don’t support running Cron jobs natively. Shoutout to Northflank again for providing such capability with great developer experience.&lt;/p&gt;

&lt;p&gt;For Koyeb, the supported regions for free tiers are quite limited at the time of writing this.&lt;/p&gt;

&lt;p&gt;Lastly, &lt;a href="https://render.com/pricing" rel="noopener noreferrer"&gt;render.com&lt;/a&gt; is another popular alternative out there in the market. You may want to check them out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Comparisons
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Updated as of 1 Dec 2022.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Free tiers
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Pricing
&lt;/h3&gt;

&lt;p&gt;Free tiers aside, most of these Heroku alternatives bill you for the resources you use &lt;strong&gt;by the second/minute/hour&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One thing that I did not cover here is &lt;strong&gt;bandwidth cost&lt;/strong&gt; (inbound + outbound). This is something you might want to seriously consider for bigger projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  They feel… different
&lt;/h2&gt;

&lt;p&gt;Perhaps I am biased. There aren’t any obvious drop-in replacements for Heroku free tiers (at least not on par with Heroku’s generosity). Throwing free tiers aside, most of them don’t offer the same kind of developer experience as Heroku.&lt;/p&gt;

&lt;p&gt;Heroku free tiers are one of the best things that have ever happened to software engineering in my opinion.&lt;/p&gt;

&lt;p&gt;I really appreciated &lt;a href="https://devcenter.heroku.com/articles/free-dyno-hours" rel="noopener noreferrer"&gt;Heroku’s free dyno hours&lt;/a&gt;. I didn’t mind the fact that dynos goes to sleep and only gets woken up when it’s needed — this means I could host multiple demos at once and only consume my free limits on demand.&lt;/p&gt;

&lt;p&gt;Need to show something to someone quickly? Simply send them your URL. It’ll be up in a couple of seconds and I am totally cool with the cold start. This developer experience is something all the existing platforms cannot give, at least not with their free tiers.&lt;/p&gt;

&lt;p&gt;While I’m a happy paying customer, it stings to pay $5/mo for something that I do not use 99% of the time.&lt;/p&gt;

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

&lt;p&gt;Sadly, it is how it is. The Internet is a gnarly place. If you are offering free service on the Internet, people will find a way to abuse it. I remember &lt;a href="https://jerrynsh.com/i-built-my-own-tiny-url/" rel="noopener noreferrer"&gt;hosting my first URL shortener&lt;/a&gt;. On my first day of making it public, the service immediately got spammed. I had to implement CAPTCHA and expiring links to mitigate abuse.&lt;/p&gt;

&lt;p&gt;Today, whenever I see something available for “truly free” (&lt;em&gt;if you know what I mean&lt;/em&gt;) created by individuals, I can’t help but wonder how are they going to keep the lights on sustainably out of goodwill. I just hate to see someone’s good intentions fail. &lt;a href="https://news.ycombinator.com/item?id=33432820" rel="noopener noreferrer"&gt;Maybe we should start paying for Internet stuff&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Though I use cloud services like AWS almost every day, PaaS like Heroku always holds a special place in my heart.&lt;/p&gt;

&lt;p&gt;Heroku was amazing. I think it did a great job of raising the bar in the PaaS field besides indirectly advocating free education (in one way or another).&lt;/p&gt;

&lt;p&gt;Thank you, Heroku, from the bottom of my heart.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://jerrynsh.com/bid-farewell-to-heroku-free-tier/" rel="noopener noreferrer"&gt;jerrynsh.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>motivation</category>
      <category>career</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Stop Comparing JWT vs Cookies</title>
      <dc:creator>Jerry Ng</dc:creator>
      <pubDate>Thu, 01 Dec 2022 23:30:43 +0000</pubDate>
      <link>https://dev.to/jerrynsh/stop-comparing-jwt-vs-cookies-31n5</link>
      <guid>https://dev.to/jerrynsh/stop-comparing-jwt-vs-cookies-31n5</guid>
      <description>&lt;p&gt;There is a lot of confusion about cookies, sessions, token-based authentication, and JWT.&lt;/p&gt;

&lt;p&gt;Today, I want to clarify what people mean when they talk about “&lt;em&gt;JWT vs Cookie&lt;/em&gt;, “&lt;em&gt;Local Storage vs Cookies&lt;/em&gt;,” “&lt;em&gt;Session vs token-based authentication&lt;/em&gt;,” and “&lt;em&gt;Bearer token vs Cookie&lt;/em&gt;” once and for all.&lt;/p&gt;

&lt;p&gt;Here’s a hint — we should stop comparing JWT and Cookies!&lt;/p&gt;

&lt;p&gt;Along the line, I’ll go through what XSS and CSRF attacks are and how to prevent them using token-based authentication with JWT and CSRF tokens.&lt;/p&gt;

&lt;p&gt;Let’s begin!&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
Terminology Speed Run

&lt;ul&gt;
&lt;li&gt;Client&lt;/li&gt;
&lt;li&gt;Server&lt;/li&gt;
&lt;li&gt;Request/Response Headers&lt;/li&gt;
&lt;li&gt;Cookie&lt;/li&gt;
&lt;li&gt;XSS Attack&lt;/li&gt;
&lt;li&gt;CSRF Attack&lt;/li&gt;
&lt;li&gt;Cookies Storage&lt;/li&gt;
&lt;li&gt;Web Storage&lt;/li&gt;
&lt;li&gt;Cookies (Storage) vs Web Storage&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

JWT

&lt;ul&gt;
&lt;li&gt;So, why use JWT?&lt;/li&gt;
&lt;li&gt;Stop comparing JWT and Cookie&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Session-based vs Token-based Authentication&lt;/li&gt;

&lt;li&gt;Cookie vs Bearer Tokens&lt;/li&gt;

&lt;li&gt;

CSRF Prevention

&lt;ul&gt;
&lt;li&gt;Same-site Cookies&lt;/li&gt;
&lt;li&gt;Common CSRF prevention methods&lt;/li&gt;
&lt;li&gt;The modified “cookie-to-header token” approach&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Conclusion

&lt;ul&gt;
&lt;li&gt;A Demo, perhaps?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Reference&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Terminology Speed Run
&lt;/h2&gt;

&lt;p&gt;To start, it’s important to know the differences between some of these terminologies.&lt;/p&gt;

&lt;p&gt;Without explicitly stating these, it would be unclear for us to compare things properly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client
&lt;/h3&gt;

&lt;p&gt;Our client application. In this context, we are specifically talking about our web browsers, e.g., Firefox, Brave, Chrome, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server
&lt;/h3&gt;

&lt;p&gt;Computers that are doing all the magic behind the curtains.&lt;/p&gt;

&lt;h3&gt;
  
  
  Request/Response Headers
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" rel="noopener noreferrer"&gt;HTTP headers&lt;/a&gt;. Note that they are case-insensitive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cookie
&lt;/h3&gt;

&lt;p&gt;Aka “HTTP cookie,” “web cookie,” or “browser cookie.”&lt;/p&gt;

&lt;p&gt;A small piece of information that a server sends back to the client.&lt;/p&gt;

&lt;p&gt;Stored in the browser’s Cookies storage, cookies are typically used for authentication, personalization, and tracking.&lt;/p&gt;

&lt;p&gt;A cookie is received in name-value pairs via the &lt;code&gt;Set-Cookie&lt;/code&gt; response header in a request. With this, your cookie will automatically be kept in the browser’s Cookies storage (&lt;code&gt;document.cookie&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F99c2ywbvmfnq37wf01yr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F99c2ywbvmfnq37wf01yr.png" alt="An example of how a Cookie is received. You should never publicly share your JWT! (this JWT is no longer in use).&amp;lt;br&amp;gt;
"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An example of how a Cookie is received. You should never publicly share your JWT! (this JWT is no longer in use).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cookies with &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, and &lt;code&gt;SameSite=Strict&lt;/code&gt; flags are more secure.&lt;/p&gt;

&lt;p&gt;For example, with the &lt;code&gt;HttpOnly&lt;/code&gt; flag, the cookies are not accessible through JavaScript, thus making it immune to XSS attacks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fg06mb8cvihdle36u4k4d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fg06mb8cvihdle36u4k4d.png" alt="With HttpOnly, the cookie is not shown&amp;lt;br&amp;gt;
"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;With HttpOnly, the cookie is not shown&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies" rel="noopener noreferrer"&gt;Read more on MDN&lt;/a&gt; and check out what other flags do (they are handy).&lt;/p&gt;

&lt;h3&gt;
  
  
  XSS Attack
&lt;/h3&gt;

&lt;p&gt;Aka “Cross-Site Scripting” attack.&lt;/p&gt;

&lt;p&gt;For context, the web storage (e.g., local storage) is accessible through JavaScript on the same domain. Consequently, web storage is vulnerable to XSS attacks.&lt;/p&gt;

&lt;p&gt;In short, XSS is a type of vulnerability where an attacker injects JavaScript that will run on your page.&lt;/p&gt;

&lt;p&gt;Basic XSS attacks attempt to inject JavaScript through form inputs, where the attacker puts an &lt;code&gt;alert(localStorage.getItem('your-secret-token'))&lt;/code&gt; into a form to see if it is run by the browser and can be viewed by other users.&lt;/p&gt;

&lt;p&gt;If you still don’t get what XSS is, check out &lt;a href="https://www.youtube.com/watch?v=EoaDgUgS6QA" rel="noopener noreferrer"&gt;this “XSS Explained” video&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSRF Attack
&lt;/h3&gt;

&lt;p&gt;Aka “Cross-Site Request Forgery” attack.&lt;/p&gt;

&lt;p&gt;Cookies are vulnerable to CSRF attacks. No cookies = no CSRF attacks.&lt;/p&gt;

&lt;p&gt;As browsers automatically send Cookies with all requests, CSRF attacks use this to gain authenticated access to a trusted site. Need more? &lt;a href="https://stackoverflow.com/a/33829607/10067850" rel="noopener noreferrer"&gt;Read CSRF in simple words&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To protect your site against CSRF attacks while using Cookies (with &lt;code&gt;SameSite=None&lt;/code&gt;), check out this &lt;a href="https://stackoverflow.com/questions/34782493/difference-between-csrf-and-x-csrf-token/34783845#34783845" rel="noopener noreferrer"&gt;StackOverflow answer&lt;/a&gt;. Next, read more about &lt;a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery#Prevention" rel="noopener noreferrer"&gt;CSRF prevention on Wikipedia&lt;/a&gt; (Wikipedia is generally too dry to my liking, but this is good!).&lt;/p&gt;

&lt;p&gt;Still don’t get what CSRF is? Check out &lt;a href="https://www.youtube.com/watch?v=eWEgUcHPle0" rel="noopener noreferrer"&gt;this “CSRF Explained” video&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cookies Storage
&lt;/h3&gt;

&lt;p&gt;Aka “Cookie Jar” or “Cookies.” &lt;em&gt;Yup. I can already see why it is confusing for many.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Client-side storage where HTTP cookies are stored.&lt;/p&gt;

&lt;p&gt;Here’s an important note: Browsers automatically send cookies (no client-side code needed) along with every request via the &lt;code&gt;cookie&lt;/code&gt; request header. This is exactly why Cookie (storage) is vulnerable to CSRF attacks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Frksbrca85jshug1d8otk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Frksbrca85jshug1d8otk.png" alt="XSS attacks can be prevented when using Cookies storage if the cookie is set with the HttpOnly flag.&amp;lt;br&amp;gt;
"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;XSS attacks can be prevented when using Cookies storage if the cookie is set with the HttpOnly flag.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media.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%2F1a9pjsnifpp9sa855uhy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F1a9pjsnifpp9sa855uhy.png" alt="To view Cookies: F12 → ‘Application’ → ‘Storage’ → ‘Cookies’&amp;lt;br&amp;gt;
"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To view Cookies: F12 → ‘Application’ → ‘Storage’ → ‘Cookies’&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Web Storage
&lt;/h3&gt;

&lt;p&gt;Aka “Local/Session Storage.”&lt;/p&gt;

&lt;p&gt;Client-side storage. They are typically used to store data in key-value pairs.&lt;/p&gt;

&lt;p&gt;Vulnerable to XSS attacks. Hence, not ideal for storing private/sensitive/authentication-related data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F4l18f6y2neqgc95iqhcv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F4l18f6y2neqgc95iqhcv.png" alt="You can get any item on your Local Storage using JavaScript&amp;lt;br&amp;gt;
"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can get any item on your Local Storage using JavaScript&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fxppb52k71mszxr83prbn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fxppb52k71mszxr83prbn.png" alt="To view Local Storage items: F12 → ‘Application’ → ‘Storage’ → ‘Local Storage’ / ’Session Storage’&amp;lt;br&amp;gt;
"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To view Local Storage items: F12 → ‘Application’ → ‘Storage’ → ‘Local Storage’ / ’Session Storage’&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;sessionStorage&lt;/code&gt; — data is persisted only for the duration of the page session&lt;/p&gt;

&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; — data is persisted even when the browser is closed and reopened&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API" rel="noopener noreferrer"&gt;Read more on MDN&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cookies (Storage) vs Web Storage
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Local/Session Storage&lt;/th&gt;
&lt;th&gt;Cookies (Storage)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;Accessible through JavaScript on the same domain&lt;/td&gt;
&lt;td&gt;Cookies, when used with the HttpOnly cookie flag, are not accessible through JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XSS attacks&lt;/td&gt;
&lt;td&gt;Vulnerable to XSS attacks&lt;/td&gt;
&lt;td&gt;Immune to XSS (with HttpOnly flag)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSRF attacks&lt;/td&gt;
&lt;td&gt;Immune to CSRF attacks&lt;/td&gt;
&lt;td&gt;Vulnerable to CSRF attacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mitigation&lt;/td&gt;
&lt;td&gt;Do not store private/sensitive/authentication-related data here&lt;/td&gt;
&lt;td&gt;Make use of CSRF tokens or &lt;a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery#Prevention" rel="noopener noreferrer"&gt;other prevention methods&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  JWT
&lt;/h2&gt;

&lt;p&gt;Aka “JSON Web Tokens.”&lt;/p&gt;

&lt;p&gt;Commonly used for &lt;a href="https://www.okta.com/identity-101/authentication-vs-authorization/#authentication-vs-authorization-2" rel="noopener noreferrer"&gt;authentication and authorization&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;JWT is an open standard (&lt;a href="https://tools.ietf.org/html/rfc7519" rel="noopener noreferrer"&gt;RFC 7519&lt;/a&gt;). Meaning all JWTs are tokens.&lt;/p&gt;

&lt;p&gt;Typically, JWT is stored in Local Storage or Cookies (storage).&lt;/p&gt;

&lt;p&gt;Remember, JWT is not encrypted by any means.&lt;/p&gt;

&lt;p&gt;Rather, it is encoded in Base64. Try decoding any JWT on &lt;a href="https://jwt.io/" rel="noopener noreferrer"&gt;jwt.io&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  So, why use JWT?
&lt;/h3&gt;

&lt;p&gt;Often used with token-based authentication, horizontal scaling is easier when using JWT.&lt;/p&gt;

&lt;p&gt;Why? The verification of JWT does not require any communication between the servers and databases. In other words, the authentication can be stateless.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://auth0.com/docs/secure/tokens/json-web-tokens" rel="noopener noreferrer"&gt;Read more on Auth0&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stop comparing JWT and Cookie
&lt;/h3&gt;

&lt;p&gt;Neither JWT nor Cookie are authentication mechanisms on their own.&lt;/p&gt;

&lt;p&gt;JWT is simply a &lt;a href="https://www.rfc-editor.org/rfc/rfc7519" rel="noopener noreferrer"&gt;token format&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A cookie is an &lt;a href="https://www.rfc-editor.org/rfc/rfc6265" rel="noopener noreferrer"&gt;HTTP state management mechanism&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As demonstrated, a web cookie can contain JWT and can be stored within your browser’s Cookies storage.&lt;/p&gt;

&lt;p&gt;So, we need to stop comparing JWT and Cookie.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session-based vs Token-based Authentication
&lt;/h2&gt;

&lt;p&gt;Rather, the right question to ask is, “what is the difference between token-based authentication and session-based authentication?”&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Token-based&lt;/th&gt;
&lt;th&gt;Session-based&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stateless&lt;/td&gt;
&lt;td&gt;Stateful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The authentication state is NOT stored anywhere on the server-side&lt;/td&gt;
&lt;td&gt;The authentication state is stored on the server-side (DB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Easier to scale horizontally&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.authgear.com/post/session-vs-token-authentication#:~:text=in%20recent%20times.-,Limited%20Scalability,-Since%20Cookies%20are" rel="noopener noreferrer"&gt;Harder to scale horizontally&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commonly uses JWT for authentication&lt;/td&gt;
&lt;td&gt;Commonly uses Session ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typically sent to the server via an HTTP Request &lt;code&gt;Authorization&lt;/code&gt; Header (e.g. &lt;code&gt;Bearer &amp;lt;token&amp;gt;)&lt;/code&gt;. Can use &lt;code&gt;Cookie&lt;/code&gt; too&lt;/td&gt;
&lt;td&gt;Usually sent to the server in the &lt;code&gt;Cookie&lt;/code&gt; request header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harder to revoke a user session&lt;/td&gt;
&lt;td&gt;Able to revoke user session with ease&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Cookie vs Bearer Tokens
&lt;/h2&gt;

&lt;p&gt;Now, we know how cookies work. Let’s take a stab at the term “Bearer tokens.” Let’s assume we’ll use JWT as our authentication token from hereon.&lt;/p&gt;

&lt;p&gt;What people call a “Bearer token” is a string (e.g., JWT) that goes into the &lt;code&gt;Authorization&lt;/code&gt; header of any HTTP request. Unlike a browser cookie, it is not automatically stored anywhere, thus making this CSRF impossible.&lt;/p&gt;

&lt;p&gt;To make use of a “Bearer token,” we’ll need to explicitly store the JWT somewhere in our client (Cookies storage or Local Storage) and add that JWT to our HTTP &lt;code&gt;Authorization&lt;/code&gt; header while making requests.&lt;/p&gt;

&lt;p&gt;If your cookie containing a JWT is set with the &lt;code&gt;HttpOnly&lt;/code&gt; flag, retrieving your token from the client side would be impossible with JavaScript.&lt;/p&gt;

&lt;p&gt;“Wait, how about we use Local Storage then?”&lt;/p&gt;

&lt;p&gt;Remember, using Local Storage makes our JWT vulnerable to XSS. As a result, you’ll often hear people advise strongly against storing JWT in Local Storage.&lt;/p&gt;

&lt;p&gt;At this point, it may sound like using Cookie to store JWT is our only option. But remember, this makes our website vulnerable to CSRF attacks!&lt;/p&gt;

&lt;h2&gt;
  
  
  CSRF Prevention
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Same-site Cookies
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite" rel="noopener noreferrer"&gt;Same-site cookies&lt;/a&gt; can effectively prevent CSRF attacks. Though, it comes with other caveats. &lt;a href="https://security.stackexchange.com/questions/121971/will-same-site-cookies-be-sufficient-protection-against-csrf-and-xss/121986#121986" rel="noopener noreferrer"&gt;Read more here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What follows assumes that we’re not going to use SameSite cookies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common CSRF prevention methods
&lt;/h3&gt;

&lt;p&gt;Leaving JWT behind for a bit, these two are some of the most common CSRF prevention methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery#Synchronizer_token_pattern" rel="noopener noreferrer"&gt;Synchronizer token pattern&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery#Cookie-to-header_token" rel="noopener noreferrer"&gt;Cookie-to-header-token pattern&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out this &lt;a href="https://stackoverflow.com/questions/34782493/difference-between-csrf-and-x-csrf-token/34783845#34783845" rel="noopener noreferrer"&gt;StackOverflow answer&lt;/a&gt; for a quick summary of the two approaches above.&lt;/p&gt;

&lt;p&gt;Cool. But now comes the question — how can we do this with JWT?&lt;/p&gt;

&lt;h3&gt;
  
  
  The modified “cookie-to-header token” approach
&lt;/h3&gt;

&lt;p&gt;Honestly, I’m not too sure if this approach has a proper name. I found this approach from this wonderful talk about &lt;a href="https://youtu.be/67mezK3NzpU" rel="noopener noreferrer"&gt;100% Stateless with JWT (JSON Web Token) by Hubert Sablonnière&lt;/a&gt;. I highly recommend you check it out. It’s worth the hour.&lt;/p&gt;

&lt;p&gt;In short, the modified approach (in my opinion) looks similar to the original &lt;a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery#Cookie-to-header_token" rel="noopener noreferrer"&gt;Cookie-to-header token approach&lt;/a&gt; except with a few tweaks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  the anti-CSRF token is returned in a separate response header (e.g. &lt;code&gt;X-CSRF-Token&lt;/code&gt;) instead of the &lt;code&gt;Set-Cookie&lt;/code&gt; response header&lt;/li&gt;
&lt;li&gt;  we sign and set a JWT on the &lt;code&gt;Set-Cookie&lt;/code&gt; response header&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fq12g94l0xajmn2c3xupg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fq12g94l0xajmn2c3xupg.png" alt="The modified “cookie-to-header token” approach"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This implementation can be found on &lt;a href="https://worker-auth.jerrynsh.workers.dev/" rel="noopener noreferrer"&gt;this Cloudflare Worker demo&lt;/a&gt; and &lt;a href="https://github.com/ngshiheng/worker-auth" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user logs in, and the server signs a JWT with &lt;code&gt;csrfToken&lt;/code&gt; as part of the JWT claim.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "email": "your@email.com",
  "exp": 1666798498,
  "csrfToken": "1449bd3e-41c2-45cb-a538-73c7ad80ca2c",
  "iat": 1666794898
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated &lt;code&gt;csrfToken&lt;/code&gt; should be unpredictable and unique per-user session.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The JWT would then be stringified into a cookie set into the &lt;code&gt;Set-Cookie&lt;/code&gt; response header. The randomly generated &lt;code&gt;csrfToken&lt;/code&gt; , on the other hand, will be set in the &lt;code&gt;X-CSRF-Token&lt;/code&gt; response header.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;With the &lt;code&gt;Set-Cookie&lt;/code&gt; header present in the response header, our browser would automatically store the JWT in the Cookies (storage). The &lt;code&gt;csrfToken&lt;/code&gt; present in the &lt;code&gt;X-CSRF-Token&lt;/code&gt; header will be extracted and set in the browser’s local storage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When a request (e.g., GET /hello) is triggered, our browser will get the &lt;code&gt;csrfToken&lt;/code&gt; from the local storage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The JWT from the Cookies (storage) and the &lt;code&gt;csrfToken&lt;/code&gt; retrieved from the local storage will be sent to the server in our request header.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The server will verify the JWT and check the &lt;code&gt;csrfToken&lt;/code&gt; from the request header with the &lt;code&gt;csrfToken&lt;/code&gt; claim inside the JWT to verify if the CSRF Token is valid.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;In essence, as long as authentication isn’t automatic (such as in browsers with Cookies), you don’t have to worry about CSRF attacks.&lt;/p&gt;

&lt;p&gt;For example, if your application attaches authentication credentials via a &lt;code&gt;Authorization&lt;/code&gt; header, CSRF isn't possible as the browser can't authenticate the request automatically.&lt;/p&gt;

&lt;p&gt;Lastly, we need to stop advocating that any of our comparisons is better. That is not how it works. Rather, we should think about the tradeoffs that we are making.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Demo, perhaps?
&lt;/h3&gt;

&lt;p&gt;The screenshots in this post are taken from &lt;a href="https://worker-auth.jerrynsh.workers.dev/" rel="noopener noreferrer"&gt;this Cloudflare Worker demo&lt;/a&gt; I made. The source code of this demo can be found &lt;a href="https://github.com/ngshiheng/worker-auth" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To test a CSRF attack, check out this &lt;a href="https://littletools.app/form?eyJtZXRob2QiOiJQT1NUIiwiYWN0aW9uIjoiaHR0cHM6Ly93b3JrZXItYXV0aC5qZXJyeW5zaC53b3JrZXJzLmRldi9oZWxsbyIsImZpZWxkcyI6W119" rel="noopener noreferrer"&gt;little tool&lt;/a&gt; (credits: &lt;a href="https://sharats.me/" rel="noopener noreferrer"&gt;Shrikant&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That aside, I created a &lt;a href="https://github.com/ngshiheng/worker-auth/tree/csrf-attack-demo" rel="noopener noreferrer"&gt;branch name &lt;code&gt;csrf-attack-demo&lt;/code&gt;&lt;/a&gt; where you could run that locally to simulate a CSRF attack on our demo site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference
&lt;/h2&gt;

&lt;p&gt;I owe my thanks to all of the following references:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://www.youtube.com/watch?v=67mezK3NzpU" rel="noopener noreferrer"&gt;100% Stateless with JWT (JSON Web Token) by Hubert Sablonnière&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://blog.ropnop.com/storing-tokens-in-browser/" rel="noopener noreferrer"&gt;How to Store Session Tokens in a Browser (and the impacts of each)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://jerrynsh.com/all-to-know-about-auth-and-cookies/" rel="noopener noreferrer"&gt;jerrynsh.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>security</category>
    </item>
    <item>
      <title>JWT vs Cookie: Why Comparing the Two Is Misleading</title>
      <dc:creator>Jerry Ng</dc:creator>
      <pubDate>Tue, 01 Nov 2022 00:00:19 +0000</pubDate>
      <link>https://dev.to/jerrynsh/jwt-vs-cookie-why-comparing-the-two-is-misleading-5166</link>
      <guid>https://dev.to/jerrynsh/jwt-vs-cookie-why-comparing-the-two-is-misleading-5166</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_uo-kPNn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://images.unsplash.com/photo-1558961363-fa8fdf82db35%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DMnwxMTc3M3wwfDF8c2VhcmNofDF8fGNvb2tpZXxlbnwwfHx8fDE2NzUyNTMyOTE%26ixlib%3Drb-4.0.3%26q%3D80%26w%3D2000" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_uo-kPNn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://images.unsplash.com/photo-1558961363-fa8fdf82db35%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DMnwxMTc3M3wwfDF8c2VhcmNofDF8fGNvb2tpZXxlbnwwfHx8fDE2NzUyNTMyOTE%26ixlib%3Drb-4.0.3%26q%3D80%26w%3D2000" alt="JWT vs Cookie: Why Comparing the Two Is Misleading" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is a lot of confusion about cookies, sessions, token-based authentication, and JWT.&lt;/p&gt;

&lt;p&gt;Today, I want to clarify what people mean when they talk about “&lt;em&gt;JWT vs Cookie&lt;/em&gt;, “&lt;em&gt;Local Storage vs Cookies&lt;/em&gt;”, “&lt;em&gt;Session vs token-based authentication&lt;/em&gt;”, and “&lt;em&gt;Bearer token vs Cookie&lt;/em&gt;” once and for all.&lt;/p&gt;

&lt;p&gt;Here’s a hint — we should stop comparing JWT vs Cookies!&lt;/p&gt;

&lt;p&gt;Along the line, I’ll go through what XSS and CSRF attacks are and how to prevent them using token-based authentication with JWT and CSRF tokens.&lt;/p&gt;

&lt;p&gt;Let's begin!&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy
&lt;/h2&gt;

&lt;p&gt;To start, it’s important to know the differences between some of these terminologies.&lt;/p&gt;

&lt;p&gt;Without explicitly stating these, it would be unclear for us to compare things properly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client
&lt;/h3&gt;

&lt;p&gt;Our client application. In this context, we are specifically talking about our web browsers, e.g. Firefox, Brave, Chrome, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server
&lt;/h3&gt;

&lt;p&gt;Computers that are doing all the magic behind the curtains.&lt;/p&gt;

&lt;h3&gt;
  
  
  Request/Response Headers
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers"&gt;HTTP headers&lt;/a&gt;. Note that they are case-insensitive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cookie
&lt;/h3&gt;

&lt;p&gt;a.k.a “HTTP cookie”, “web cookie”, or “browser cookie”.&lt;/p&gt;

&lt;p&gt;A small piece of information that a server sends back to the client.&lt;/p&gt;

&lt;p&gt;Stored in the browser’s Cookies storage, cookies are typically used for authentication, personalization, and tracking.&lt;/p&gt;

&lt;p&gt;A cookie is received in name-value pairs via the &lt;code&gt;Set-Cookie&lt;/code&gt; response header in a request. With this, your cookie will automatically be kept in the browser’s Cookies storage (&lt;code&gt;document.cookie&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--q9pswdxm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2022/10/image-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--q9pswdxm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2022/10/image-2.png" alt="JWT vs Cookie: Why Comparing the Two Is Misleading" width="724" height="610"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;An example of how a Cookie is received. You should never publicly share your JWT! (this JWT is no longer in use).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Cookies with &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, and &lt;code&gt;SameSite=Strict&lt;/code&gt; flags are more secure.&lt;/p&gt;

&lt;p&gt;For example, with the &lt;code&gt;HttpOnly&lt;/code&gt; flag, the cookies are not accessible through JavaScript, thus making it immune to XSS attacks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1_LIIIOI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2022/10/image-3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1_LIIIOI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2022/10/image-3.png" alt="JWT vs Cookie: Why Comparing the Two Is Misleading" width="450" height="169"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;With HttpOnly, the cookie is not shown&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies"&gt;Read more on MDN&lt;/a&gt; and check out what other flags do (they are handy).&lt;/p&gt;
&lt;h3&gt;
  
  
  XSS Attack
&lt;/h3&gt;

&lt;p&gt;a.k.a “ &lt;strong&gt;Cross&lt;/strong&gt; - &lt;strong&gt;S&lt;/strong&gt; ite &lt;strong&gt;S&lt;/strong&gt; cripting” attack.&lt;/p&gt;

&lt;p&gt;For context, the Web Storage (e.g. Local Storage) is accessible through JavaScript on the same domain. Consequently, Web Storage is vulnerable to XSS attacks.&lt;/p&gt;

&lt;p&gt;In short, XSS is a type of vulnerability where an attacker &lt;strong&gt;injects JavaScript&lt;/strong&gt; that will run on your page.&lt;/p&gt;

&lt;p&gt;Basic XSS attacks attempt to inject JavaScript through form inputs, where the attacker puts an &lt;code&gt;alert(localStorage.getItem('your-secret-token'))&lt;/code&gt; into a form to see if it is run by the browser and can be viewed by other users.&lt;/p&gt;

&lt;p&gt;If you still don’t get what XSS is, check out &lt;a href="https://www.youtube.com/watch?v=EoaDgUgS6QA"&gt;this “XSS Explained” video&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  CSRF Attack
&lt;/h3&gt;

&lt;p&gt;a.k.a “ &lt;strong&gt;C&lt;/strong&gt; ross- &lt;strong&gt;S&lt;/strong&gt; ite &lt;strong&gt;R&lt;/strong&gt; equest &lt;strong&gt;F&lt;/strong&gt; orgery” attack.&lt;/p&gt;

&lt;p&gt;Cookies are vulnerable to CSRF attacks. &lt;strong&gt;No cookies = no CSRF attacks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;As browsers automatically send Cookies with all requests, CSRF attacks make use of this to gain authenticated access to a trusted site. Need more? &lt;a href="https://stackoverflow.com/a/33829607/10067850"&gt;Read CSRF in simple words&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To protect your site against CSRF attacks while using Cookies (with &lt;code&gt;SameSite=None&lt;/code&gt;), check out this &lt;a href="https://stackoverflow.com/questions/34782493/difference-between-csrf-and-x-csrf-token/34783845#34783845"&gt;StackOverflow answer&lt;/a&gt;. Next, read more about &lt;a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery#Prevention"&gt;CSRF prevention on Wikipedia&lt;/a&gt; (Wikipedia is generally too dry to my liking, but this is really good!).&lt;/p&gt;

&lt;p&gt;Still don’t get what CSRF is? Check out &lt;a href="https://www.youtube.com/watch?v=eWEgUcHPle0"&gt;this “CSRF Explained” video&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Cookies Storage
&lt;/h3&gt;

&lt;p&gt;a.k.a “Cookie Jar”, or “Cookies”. &lt;em&gt;Yup. I can already see why it is confusing for many.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Client-side storage where HTTP cookies are stored.&lt;/p&gt;

&lt;p&gt;Here’s an important note — &lt;strong&gt;browsers automatically send cookies&lt;/strong&gt; (no client-side code needed) along with every request via the &lt;code&gt;cookie&lt;/code&gt; request header. This is exactly why Cookie (storage) is vulnerable to CSRF attacks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dKul3A3v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2022/10/image-4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dKul3A3v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2022/10/image-4.png" alt="JWT vs Cookie: Why Comparing the Two Is Misleading" width="722" height="362"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;XSS attacks can be prevented when using Cookies storage if the cookie is set with the HttpOnly flag.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ymUoaTBr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2022/10/image-5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ymUoaTBr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2022/10/image-5.png" alt="JWT vs Cookie: Why Comparing the Two Is Misleading" width="740" height="283"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;To view Cookies: F12 → ‘Application’ → ‘Storage’ → ‘Cookies’&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Web Storage
&lt;/h3&gt;

&lt;p&gt;a.k.a “Local/Session Storage”.&lt;/p&gt;

&lt;p&gt;Client-side storage. They are typically used to store data in key-value pairs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vulnerable to XSS&lt;/strong&gt; attacks. Hence, not ideal for storing private/sensitive/authentication-related data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BJtgq5BT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2022/10/image-6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BJtgq5BT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2022/10/image-6.png" alt="JWT vs Cookie: Why Comparing the Two Is Misleading" width="463" height="152"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;You can get any item on your Local Storage using JavaScript&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kjqA_jGz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2022/10/image-7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kjqA_jGz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2022/10/image-7.png" alt="JWT vs Cookie: Why Comparing the Two Is Misleading" width="731" height="352"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;To view Local Storage items: F12 → ‘Application’ → ‘Storage’ → ‘Local Storage’ / ’Session Storage’&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sessionStorage&lt;/code&gt; — data is persisted only for the duration of the page session&lt;/p&gt;

&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; — data is persisted even when the browser is closed and reopened&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API"&gt;Read more on MDN&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Cookies (Storage) vs Web Storage
&lt;/h3&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Local/Session Storage&lt;/th&gt;
&lt;th&gt;Cookies (Storage)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;Accessible through JavaScript on the same domain&lt;/td&gt;
&lt;td&gt;Cookies, when used with the HttpOnly cookie flag, are not accessible through JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XSS attacks&lt;/td&gt;
&lt;td&gt;Vulnerable to XSS attacks&lt;/td&gt;
&lt;td&gt;Immune to XSS (with HttpOnly flag)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSRF attacks&lt;/td&gt;
&lt;td&gt;Immune to CSRF attacks&lt;/td&gt;
&lt;td&gt;Vulnerable to CSRF attacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mitigation&lt;/td&gt;
&lt;td&gt;Do not store private/sensitive/authentication-related data here&lt;/td&gt;
&lt;td&gt;Make use of CSRF tokens or &lt;a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery#Prevention"&gt;other prevention methods&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  JWT
&lt;/h2&gt;

&lt;p&gt;a.k.a “JSON Web Tokens”.&lt;/p&gt;

&lt;p&gt;Commonly used for &lt;a href="https://www.okta.com/identity-101/authentication-vs-authorization/#authentication-vs-authorization-2"&gt;authentication and authorization&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;JWT is an open standard (&lt;a href="https://tools.ietf.org/html/rfc7519"&gt;RFC 7519&lt;/a&gt;). Meaning all JWTs are tokens.&lt;/p&gt;

&lt;p&gt;Typically, JWT is stored in Local Storage or Cookies (storage).&lt;/p&gt;

&lt;p&gt;Remember, &lt;strong&gt;JWT is&lt;/strong&gt;  &lt;strong&gt;not encrypted&lt;/strong&gt; by any means. Rather, it is encoded in Base64. Try decoding any JWT on &lt;a href="https://jwt.io/"&gt;jwt.io&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  So, why use JWT?
&lt;/h3&gt;

&lt;p&gt;Often used with token-based authentication, horizontal scaling is easier when using JWT.&lt;/p&gt;

&lt;p&gt;Why? The verification of JWT does not require any communication between the servers and databases. In other words, the authentication can be &lt;strong&gt;stateless&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://auth0.com/docs/secure/tokens/json-web-tokens"&gt;Read more on Auth0&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Stop comparing JWT &amp;amp; Cookie
&lt;/h3&gt;

&lt;p&gt;Neither JWT nor Cookie are authentication mechanisms on their own.&lt;/p&gt;

&lt;p&gt;JWT is simply a &lt;a href="https://www.rfc-editor.org/rfc/rfc7519"&gt;token format&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A cookie is an &lt;a href="https://www.rfc-editor.org/rfc/rfc6265"&gt;HTTP state management mechanism&lt;/a&gt; really.&lt;/p&gt;

&lt;p&gt;As demonstrated, a web cookie can contain JWT and can be stored within your browser’s Cookies storage.&lt;/p&gt;

&lt;p&gt;So, we need to stop comparing JWT vs Cookie.&lt;/p&gt;
&lt;h2&gt;
  
  
  Session-based vs Token-based Authentication
&lt;/h2&gt;

&lt;p&gt;Rather, the right question to ask is “What is the difference between token-based authentication and session-based authentication?”&lt;/p&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Token-based&lt;/th&gt;
&lt;th&gt;Session-based&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stateless&lt;/td&gt;
&lt;td&gt;Stateful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The authentication state is NOT stored anywhere on the server-side&lt;/td&gt;
&lt;td&gt;The authentication state is stored on the server side (DB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Easier to scale horizontally&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.authgear.com/post/session-vs-token-authentication#:~:text=in%20recent%20times.-,Limited%20Scalability,-Since%20Cookies%20are"&gt;Harder to scale horizontally&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commonly uses JWT for authentication&lt;/td&gt;
&lt;td&gt;Commonly uses Session ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typically sent to the server via an HTTP Request &lt;code&gt;Authorization&lt;/code&gt; Header (e.g. &lt;code&gt;Bearer &amp;lt;token&amp;gt;)&lt;/code&gt;. Can use &lt;code&gt;Cookie&lt;/code&gt; too&lt;/td&gt;
&lt;td&gt;Usually sent to the server in the &lt;code&gt;Cookie&lt;/code&gt; request header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harder to revoke a user session&lt;/td&gt;
&lt;td&gt;Able to revoke user session with ease&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  Cookie vs Bearer Tokens
&lt;/h2&gt;

&lt;p&gt;Now, we know how cookies work. Let’s take a stab at the term “Bearer tokens”. Let’s assume we’ll use JWT as our authentication token from hereon.&lt;/p&gt;

&lt;p&gt;What people call a “Bearer token” is a string (e.g. JWT) that goes into the &lt;code&gt;Authorization&lt;/code&gt; header of any HTTP request. Unlike a browser cookie, it is not automatically stored anywhere, thus making this &lt;strong&gt;CSRF impossible&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET &amp;lt;http://www.example.com&amp;gt;
Authorization: Bearer my_bearer_token_value // HTTP Request Header

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

&lt;/div&gt;



&lt;p&gt;To make use of a “Bearer token”, we’ll need to explicitly store the JWT somewhere in our client (Cookies storage or Local Storage) and add that JWT to our HTTP &lt;code&gt;Authorization&lt;/code&gt; header while making requests.&lt;/p&gt;

&lt;p&gt;If your cookie (e.g. with a JWT) is set with the &lt;code&gt;HttpOnly&lt;/code&gt; flag, retrieving your token from the client side would be impossible with JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  “Wait, how about we use Local Storage then?”
&lt;/h3&gt;

&lt;p&gt;Remember, using Local Storage makes our JWT vulnerable to XSS. As a result, you’ll often hear people advise strongly against storing JWT in Local Storage.&lt;/p&gt;

&lt;p&gt;At this point, it may sound like using Cookie to store JWT is our only option. But remember, this makes our website vulnerable to CSRF attacks!&lt;/p&gt;

&lt;h2&gt;
  
  
  CSRF Prevention
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Same-site Cookies
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite"&gt;Same-site cookies&lt;/a&gt; can effectively prevent CSRF attacks. Though, it comes with other caveats. &lt;a href="https://security.stackexchange.com/questions/121971/will-same-site-cookies-be-sufficient-protection-against-csrf-and-xss/121986#121986"&gt;Read more here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What follows assumes that we're not going to use SameSite cookies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common CSRF prevention methods
&lt;/h3&gt;

&lt;p&gt;Leaving JWT behind for a bit, these 2 are some of the most common CSRF prevention methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery#Synchronizer_token_pattern"&gt;Synchronizer token pattern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery#Cookie-to-header_token"&gt;Cookie-to-header-token pattern&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out this &lt;a href="https://stackoverflow.com/questions/34782493/difference-between-csrf-and-x-csrf-token/34783845#34783845"&gt;StackOverflow answer&lt;/a&gt; for a quick summary of the 2 approaches above.&lt;/p&gt;

&lt;p&gt;Cool. But now comes the question — how can we do this with JWT?&lt;/p&gt;

&lt;h3&gt;
  
  
  The Modified "Cookie-to-header token" Approach
&lt;/h3&gt;

&lt;p&gt;Honestly, I’m not too sure if this approach has a proper name. I found this approach from this wonderful talk about &lt;a href="https://youtu.be/67mezK3NzpU"&gt;100% Stateless with JWT (JSON Web Token) by Hubert Sablonnière&lt;/a&gt;. I highly, highly recommend you to check it out. It’s worth the hour.&lt;/p&gt;

&lt;p&gt;In short, the modified approach (in my opinion) looks similar to the original &lt;a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery#Cookie-to-header_token"&gt;Cookie-to-header token approach&lt;/a&gt; except with a few tweaks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the anti-CSRF token is returned in a separate response header (e.g. &lt;code&gt;X-CSRF-Token&lt;/code&gt;) instead of the &lt;code&gt;Set-Cookie&lt;/code&gt; response header&lt;/li&gt;
&lt;li&gt;we sign and set a JWT on the &lt;code&gt;Set-Cookie&lt;/code&gt; response header&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BDUjGNfE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2022/10/image-8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BDUjGNfE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jerrynsh.com/content/images/2022/10/image-8.png" alt="JWT vs Cookie: Why Comparing the Two Is Misleading" width="800" height="501"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;This implementation can be found on this Cloudflare Worker demo and GitHub.&lt;/em&gt;&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;The user logs in, the server would sign a JWT with &lt;code&gt;csrfToken&lt;/code&gt; as part of the JWT claim (for verification in Step 6).
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "email": "your@email.com",
  "exp": 1666798498,
  "csrfToken": "1449bd3e-41c2-45cb-a538-73c7ad80ca2c",
  "iat": 1666794898
}

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

&lt;/div&gt;



&lt;p&gt;The generated &lt;code&gt;csrfToken&lt;/code&gt; should be unpredictable and unique per-user session.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The JWT would then be stringified into a cookie which will be set into the &lt;code&gt;Set-Cookie&lt;/code&gt; response header. The randomly generated &lt;code&gt;csrfToken&lt;/code&gt; on the other hand will be set in the &lt;code&gt;X-CSRF-Token&lt;/code&gt; response header.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;With the &lt;code&gt;Set-Cookie&lt;/code&gt; header present in the response header, our browser would automatically store the JWT in the Cookies (storage). The &lt;code&gt;csrfToken&lt;/code&gt; present in the &lt;code&gt;X-CSRF-Token&lt;/code&gt; header will be extracted and set in the browser’s Local Storage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When a request (e.g. GET /hello) is triggered, our browser will fetch the &lt;code&gt;csrfToken&lt;/code&gt; from the Local Storage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The JWT from the Cookies (storage) and the &lt;code&gt;csrfToken&lt;/code&gt; retrieved from the Local Storage will be sent to the server in the request header.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The server will verify the JWT and check  &lt;code&gt;csrfToken&lt;/code&gt; from the request header against the &lt;code&gt;csrfToken&lt;/code&gt; claim inside the JWT to verify if the CSRF Token is valid.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The screenshots in this post are taken from &lt;a href="https://worker-auth.jerrynsh.workers.dev/"&gt;this Cloudflare Worker demo&lt;/a&gt; I made. The source code of this demo can be found on GitHub.&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;GitHub - ngshiheng/worker-auth: To demonstrate and implement a PoC to protect your site against XSS &amp;amp; CSRF attacks.&lt;/p&gt;

&lt;p&gt;To demonstrate and implement a PoC to protect your site against XSS &amp;amp; CSRF attacks. - GitHub - ngshiheng/worker-auth: To demonstrate and implement a PoC to protect your site against XSS &amp;amp; C...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vxfVixPa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://github.com/fluidicon.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vxfVixPa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://github.com/fluidicon.png" alt="JWT vs Cookie: Why Comparing the Two Is Misleading" width="512" height="512"&gt;&lt;/a&gt;GitHubngshiheng&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AKK-Sb0v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://opengraph.githubassets.com/05f635bb7d357df2cec403267207b2a31ae018ac922945efdd454f3e9229f17f/ngshiheng/worker-auth" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AKK-Sb0v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://opengraph.githubassets.com/05f635bb7d357df2cec403267207b2a31ae018ac922945efdd454f3e9229f17f/ngshiheng/worker-auth" alt="JWT vs Cookie: Why Comparing the Two Is Misleading" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
](&lt;a href="https://github.com/ngshiheng/worker-auth"&gt;https://github.com/ngshiheng/worker-auth&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;To test a CSRF attack, check out this &lt;a href="https://littletools.app/form?eyJtZXRob2QiOiJQT1NUIiwiYWN0aW9uIjoiaHR0cHM6Ly93b3JrZXItYXV0aC5qZXJyeW5zaC53b3JrZXJzLmRldi9oZWxsbyIsImZpZWxkcyI6W119"&gt;little tool&lt;/a&gt; (credits: &lt;a href="https://sharats.me/"&gt;Shrikant&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;I created a &lt;a href="https://github.com/ngshiheng/worker-auth/commit/a781f97d95053bae182ca0841bb6d030d86d3cd1#commitcomment-97975597"&gt;branch name &lt;code&gt;csrf-attack-demo&lt;/code&gt;&lt;/a&gt; where you could run that locally to simulate a CSRF attack on your site.&lt;/p&gt;

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

&lt;p&gt;In essence, as long as authentication isn't automatic (such as in browsers with Cookies), you don't have to worry about CSRF attacks.&lt;/p&gt;

&lt;p&gt;For example, if your application attaches authentication credentials via a &lt;code&gt;Authorization&lt;/code&gt; header, then CSRF isn't possible as the browser can't automatically authenticate the request.&lt;/p&gt;

&lt;p&gt;Lastly, we need to stop advocating that any one of our comparisons is better than another. That is not how it works. Rather, we should think about the tradeoffs that we are making.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference
&lt;/h2&gt;

&lt;p&gt;I owe my thanks to all of the following references:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=67mezK3NzpU"&gt;100% Stateless with JWT (JSON Web Token) by Hubert Sablonnière&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.ropnop.com/storing-tokens-in-browser/"&gt;How to Store Session Tokens in a Browser (and the impacts of each)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>auth</category>
      <category>javascript</category>
      <category>cloudflareworker</category>
    </item>
    <item>
      <title>4 Levels of How To Use Makefile</title>
      <dc:creator>Jerry Ng</dc:creator>
      <pubDate>Mon, 31 Oct 2022 14:56:06 +0000</pubDate>
      <link>https://dev.to/jerrynsh/4-levels-of-how-to-use-makefile-4oad</link>
      <guid>https://dev.to/jerrynsh/4-levels-of-how-to-use-makefile-4oad</guid>
      <description>&lt;p&gt;I love Makefile. Today, I use Makefile for most of the projects that I am working on. You may have seen &lt;code&gt;Makefile&lt;/code&gt; in many open-source projects on GitHub (&lt;a href="https://github.com/scikit-learn/scikit-learn/blob/main/Makefile"&gt;e.g. this&lt;/a&gt;). Probably like me, you have wondered what Makefiles are and what they do.&lt;/p&gt;

&lt;p&gt;There is a bazillion of incredible Makefile tutorials out there. My goal is to get you interested enough to start using Makefile in under 5 minutes. By the end, you should know enough to start using and self-learn about Makefile.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TL;DR: read Level 1 &amp;amp; Level 2&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Make and Makefile
&lt;/h2&gt;

&lt;p&gt;In short, &lt;code&gt;Makefile&lt;/code&gt; is a special format file with &lt;em&gt;rules&lt;/em&gt; that tells the GNU Make utility tool (i.e. &lt;code&gt;make&lt;/code&gt;) how to execute commands that run on *nix. Typically, Make is used to compile, build, or install the software.&lt;/p&gt;

&lt;p&gt;While Makefile is commonly used to compile C or C++, it is NOT limited to any particular programming language. You can use Make for all sorts of stuff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Execute a chain of commands to set up your dev environment&lt;/li&gt;
&lt;li&gt;  Automate build&lt;/li&gt;
&lt;li&gt;  Run test suites&lt;/li&gt;
&lt;li&gt;  Deployments, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Use Makefile
&lt;/h2&gt;

&lt;p&gt;Compiling source code can be cumbersome, especially when you have to include multiple source files.&lt;/p&gt;

&lt;p&gt;At its core, &lt;code&gt;Makefile&lt;/code&gt; is a utility for writing and executing a series of command-line instructions for things like compiling code, testing code, formatting code, running code, etc.&lt;/p&gt;

&lt;p&gt;Basically, it helps to automate your development workflows into simple commands (&lt;code&gt;make build&lt;/code&gt;, &lt;code&gt;make test&lt;/code&gt;, &lt;code&gt;make format&lt;/code&gt;, &lt;code&gt;make run&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;make&lt;/code&gt; is preinstalled at most *nix systems that you can find&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;make&lt;/code&gt; is programming language/framework agnostic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alright enough talking, let’s get to the real deal.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Quick Guide to Makefile
&lt;/h2&gt;

&lt;p&gt;While going through the levels below, I highly encourage you to create a &lt;code&gt;Makefile&lt;/code&gt; (note: always name it &lt;code&gt;Makefile&lt;/code&gt;) and try things out yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 1: “Tell me all I need to know”
&lt;/h3&gt;

&lt;p&gt;At this level, you’ll learn the basics of Makefile; probably all you ever need to know to benefit from it.&lt;/p&gt;

&lt;p&gt;Suppose you have a project using that uses Docker. To build and run your app iteratively using a Docker container, you would typically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;docker build&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Make sure there are no running container&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;docker run&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To do this yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; image-name &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;ENV_VAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;foo
docker stop container-name &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker &lt;span class="nb"&gt;rm &lt;/span&gt;container-name &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ANOTHER_VAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bar--name container-name image-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What a hassle! There is a lot to remember and type. On top of that, many chances to make silly mistakes.&lt;/p&gt;

&lt;p&gt;Sure, you could just run all 3 commands every time you make changes. That would work. However, it is just so inefficient. Instead, we could write a &lt;code&gt;Makefile&lt;/code&gt; just like the below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;all&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;build stop run &lt;/span&gt;&lt;span class="c"&gt;#&lt;/span&gt;&lt;span class="nf"&gt; build -&amp;gt; stop -&amp;gt; run&lt;/span&gt;

&lt;span class="nl"&gt;build&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; image-name &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;ENV_VAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;foo

&lt;span class="nl"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;docker stop container-name &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker &lt;span class="nb"&gt;rm &lt;/span&gt;container-name &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="nl"&gt;run&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ANOTHER_VAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bar--name container-name image-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now to build and run a new Docker image, all you need is a single &lt;code&gt;make all&lt;/code&gt; command. In the case above, we can just call &lt;code&gt;make&lt;/code&gt; because &lt;code&gt;all&lt;/code&gt; is the first rule (note: the first rule is selected by default).&lt;/p&gt;

&lt;p&gt;To summarize, a rule within a &lt;code&gt;Makefile&lt;/code&gt; generally looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# run `make &amp;lt;target&amp;gt;` to run this rule
&lt;/span&gt;&lt;span class="nl"&gt;&amp;lt;target&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="nf"&gt;&amp;lt;prerequisite 1&amp;gt; &amp;lt;prerequisite 2&amp;gt; &amp;lt;prerequisite N&amp;gt; &lt;/span&gt;&lt;span class="c"&gt;#&lt;/span&gt;&lt;span class="nf"&gt; a comment block is prefixed with a "&lt;/span&gt;&lt;span class="c"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;"&lt;/span&gt;
    &amp;lt;&lt;span class="nb"&gt;command &lt;/span&gt;1&amp;gt;
    &amp;lt;&lt;span class="nb"&gt;command &lt;/span&gt;2&amp;gt;
    &amp;lt;&lt;span class="nb"&gt;command &lt;/span&gt;N&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaways for Level 1:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;target&amp;gt;&lt;/code&gt; — typically file name; could be any name&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;command&amp;gt;&lt;/code&gt; — typically *nix commands/steps used to make the &lt;code&gt;&amp;lt;target&amp;gt;&lt;/code&gt;. These MUST start with a TAB character&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;prerequisite&amp;gt;&lt;/code&gt; — &lt;em&gt;optional.&lt;/em&gt; This tells &lt;code&gt;make&lt;/code&gt; that each prerequisite must exist before the commands are run. Therefore, prerequisites run in order of 1 to N (in the example above).&lt;/li&gt;
&lt;li&gt;What is the &lt;code&gt;@&lt;/code&gt; syntax for? If a command line starts with &lt;code&gt;@&lt;/code&gt;, the echoing of that command is suppressed (&lt;a href="https://www.gnu.org/software/make/manual/html_node/Echoing.html"&gt;reference&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The first &lt;code&gt;&amp;lt;target&amp;gt;&lt;/code&gt; is selected by default when running just &lt;code&gt;make&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s it. Simple right?&lt;/p&gt;

&lt;p&gt;Now go forth and &lt;em&gt;make&lt;/em&gt; a Makefile at work!&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 2: “Cool, but I need a little bit more”
&lt;/h3&gt;

&lt;p&gt;The use of variable substitution is rather common when it comes to all aspects of programming. Makefile is not exempted.&lt;/p&gt;

&lt;p&gt;So, how to use environment variables (with default)?&lt;/p&gt;

&lt;p&gt;Suppose you want to &lt;code&gt;docker build&lt;/code&gt; your app with different build arguments (e.g. &lt;code&gt;ENV_VAR&lt;/code&gt;), here’s how your &lt;code&gt;Makefile&lt;/code&gt; would look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;NAME&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; my-app
&lt;span class="nv"&gt;DOCKER&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nf"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;shell&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; docker 2&amp;gt; /dev/null&lt;span class="nf"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;ENV_VAR&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nf"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;shell&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ENV_VAR&lt;/span&gt;&lt;span class="p"&gt;-development&lt;/span&gt;&lt;span class="nf"&gt;}&lt;/span&gt;&lt;span class="p"&gt;) # NOTE&lt;/span&gt;:&lt;span class="p"&gt; double &lt;/span&gt;&lt;span class="nv"&gt;$ for&lt;/span&gt;&lt;span class="p"&gt; escaping&lt;/span&gt;
&lt;span class="nv"&gt;ENV_ANOTHER_VAR&lt;/span&gt; &lt;span class="o"&gt;?=&lt;/span&gt;&lt;span class="p"&gt; bar&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;
&lt;span class="nl"&gt;build&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; build docker image based on shell ENV_VAR&lt;/span&gt;
    &lt;span class="p"&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="nv"&gt;$(DOCKER)&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;"docker is missing."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt; &lt;span class="c"&gt;# tip&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nv"&gt;$(NAME)&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;ENV_VAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(ENV_VAR)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaways for Level 2:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;.PHONY&lt;/code&gt; — by default &lt;code&gt;make&lt;/code&gt; assumes your &lt;code&gt;&amp;lt;target&amp;gt;&lt;/code&gt; is a file. So if they are not, just mark them with &lt;code&gt;.PHONY&lt;/code&gt; in case you have a filename that is the same as &lt;code&gt;&amp;lt;target&amp;gt;&lt;/code&gt; (&lt;a href="https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html"&gt;reference&lt;/a&gt; and &lt;a href="https://makefiletutorial.com/#phony"&gt;read this&lt;/a&gt; for more info)&lt;/li&gt;
&lt;li&gt;Declare a Makefile variable with &lt;code&gt;=&lt;/code&gt; or &lt;code&gt;:=&lt;/code&gt; syntax (&lt;a href="https://stackoverflow.com/questions/4879592/whats-the-difference-between-and-in-makefile"&gt;reference&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:=&lt;/code&gt; — to execute a statement once&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;=&lt;/code&gt; — to execute a statement every time. An example use case is when you need a new &lt;code&gt;date&lt;/code&gt; value every time you call a function.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;?=&lt;/code&gt; — set the &lt;em&gt;Makefile variable&lt;/em&gt; only if it's not set or doesn't have a value (&lt;a href="https://www.gnu.org/software/make/manual/html_node/Setting.html"&gt;reference&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Finally, you can use an environment variable to check if a command exists too as shown in the &lt;code&gt;if&lt;/code&gt; statement above&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Be careful about adding in-line comments as below. You may end up adding extra space to your variable:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;FOO &lt;span class="o"&gt;=&lt;/span&gt; /my/path/to &lt;span class="c"&gt;# &amp;lt;- a white space!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;echo $${ENV_VAR-development}&lt;/code&gt; sets &lt;code&gt;ENV_VAR&lt;/code&gt; &lt;em&gt;shell variable&lt;/em&gt; based on your current shell environment with a default value to &lt;code&gt;development&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Alternatively, &lt;code&gt;make&lt;/code&gt; allows you to pass variables and environment variables from the command line, e.g. &lt;code&gt;ENV_VAR=development make build&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By now, you probably have everything you need to create a Makefile for small and medium-sized projects.&lt;/p&gt;

&lt;p&gt;Now go forth and &lt;em&gt;make&lt;/em&gt; awesomeness with Makefile!&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 3: “Now, show me the fancy stuff”
&lt;/h3&gt;

&lt;p&gt;This is probably my favorite part. Everyone loves a good &lt;code&gt;help&lt;/code&gt; message right? &lt;a href="https://knowyourmeme.com/memes/for-the-better-right"&gt;Right…?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s create a useful &lt;code&gt;help&lt;/code&gt; target in case our users need help on how to use Make in our project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;.DEFAULT_GOAL&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;help&lt;/span&gt;
&lt;span class="nl"&gt;help&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Welcome to &lt;/span&gt;&lt;span class="nv"&gt;$(NAME)&lt;/span&gt;&lt;span class="s2"&gt;!"&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Use 'make &amp;lt;target&amp;gt;' where &amp;lt;target&amp;gt; is one of:"&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  all    run build -&amp;gt; stop -&amp;gt; run"&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  build  build docker image based on shell ENV_VAR"&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  stop   stop docker container"&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  run    run docker container"&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Go forth and make something great!"&lt;/span&gt;

&lt;span class="nl"&gt;all&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;build stop run&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;
&lt;span class="nl"&gt;build&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; image-name &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;ENV_VAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;foo

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;
&lt;span class="nl"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;docker stop container-name &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker &lt;span class="nb"&gt;rm &lt;/span&gt;container-name &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;
&lt;span class="nl"&gt;run&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ANOTHER_VAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bar--name container-name image-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;.DEFAULT_GOAL&lt;/code&gt; — remember how I said the first rule is selected by default when you run just &lt;code&gt;make&lt;/code&gt;? You can override that with this&lt;/li&gt;
&lt;li&gt;  With this, you can simply run &lt;code&gt;make&lt;/code&gt; to display the error message every time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, every time you add a new target to your &lt;code&gt;Makefile&lt;/code&gt;, you’ll need to add a new line of echo.&lt;/p&gt;

&lt;p&gt;Now, how does a self-documenting Makefile help message sound to you?&lt;/p&gt;

&lt;p&gt;Modify your &lt;code&gt;help&lt;/code&gt; target as follow (modify accordingly):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;.DEFAULT_GOAL&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;help&lt;/span&gt;
&lt;span class="nl"&gt;help&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; display this help message&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m&amp;lt;target&amp;gt;\033[0m\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-10s\033[0m %s\n", $$1, $$2 }'&lt;/span&gt; &lt;span class="nv"&gt;$(MAKEFILE_LIST)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, add comments with the &lt;code&gt;##&lt;/code&gt; tag to print the comments as part of the &lt;code&gt;help&lt;/code&gt; message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;all&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;build stop run &lt;/span&gt;&lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; run build -&amp;gt; stop -&amp;gt; run&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;
&lt;span class="nl"&gt;build&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; build docker image&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; image-name &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;ENV_VAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;foo

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;
&lt;span class="nl"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; stop running container&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;docker stop container-name &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker &lt;span class="nb"&gt;rm &lt;/span&gt;container-name &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;
&lt;span class="nl"&gt;run&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; run docker container&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ANOTHER_VAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bar--name container-name image-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bam! Here you have it, a nice self-documenting &lt;code&gt;help&lt;/code&gt; message:&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="nv"&gt;$ &lt;/span&gt;make

Usage:
  make &amp;lt;target&amp;gt;

Targets:
  &lt;span class="nb"&gt;help        &lt;/span&gt;display this &lt;span class="nb"&gt;help
  &lt;/span&gt;all         run build -&amp;gt; stop -&amp;gt; run
  build       build docker image
  stop        stop running container
  run         run docker container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Credits: I would like to thank the author of &lt;a href="https://www.thapaliya.com/en/writings/well-documented-makefiles/"&gt;make help - Well documented Makefiles&lt;/a&gt; for this.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 4: “Your article does not provide the level of details I need!”
&lt;/h3&gt;

&lt;p&gt;Here are some Makefile references that I’ve curated that I highly recommend you to check out; I owe my thanks to all of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://makefiletutorial.com/"&gt;Learn Makefiles With the tastiest examples&lt;/a&gt; — a really good general Makefile tutorial; I prefer to rather than going through the 180+ pages long manual&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://blog.mathieu-leplatre.info/tips-for-your-makefile-with-python.html"&gt;Tips for your Makefile with Python&lt;/a&gt; — if you’re working on a Python project&lt;/li&gt;
&lt;li&gt;  Need a decent example? Here’s a &lt;a href="https://gist.github.com/isaacs/62a2d1825d04437c6f08"&gt;Makefile with 1.8k+ Stars on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  If all else fails — &lt;a href="https://www.gnu.org/software/make/manual/make.html"&gt;GNU Make manual&lt;/a&gt; (you probably won’t need most of it)&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;Even if you’re writing good documentation, chances are Makefile is much more reliable. The beauty of Makefile is that it’s simply a rigorous way of documenting what you’re already doing.&lt;/p&gt;

&lt;p&gt;Contributing to an open-source project? Refer to the &lt;code&gt;Makefile&lt;/code&gt; to identify the development workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current State of Makefile
&lt;/h2&gt;

&lt;p&gt;First appearing in 1976, the use of Makefile is known to have its quirks.&lt;/p&gt;

&lt;p&gt;Some of the most common complaints about Make is that it has rather complicated syntax. As a result, you’ll find people who &lt;a href="https://news.ycombinator.com/item?id=21566530"&gt;absolutely love&lt;/a&gt; or &lt;a href="https://news.ycombinator.com/item?id=7463048"&gt;hate&lt;/a&gt; Makefile.&lt;/p&gt;

&lt;p&gt;I think this kind of utility gets very personal. Everyone has their favorite. I for one am less interested in having to download yet another fancy program/tool/framework/library for many other reasons.&lt;/p&gt;

&lt;p&gt;Makefile “just works”.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>bash</category>
      <category>tooling</category>
      <category>makefile</category>
    </item>
    <item>
      <title>How To Automate Ghost Blog Backup</title>
      <dc:creator>Jerry Ng</dc:creator>
      <pubDate>Mon, 22 Aug 2022 00:06:00 +0000</pubDate>
      <link>https://dev.to/jerrynsh/how-to-automate-ghost-blog-backup-1p58</link>
      <guid>https://dev.to/jerrynsh/how-to-automate-ghost-blog-backup-1p58</guid>
      <description>&lt;p&gt;I’ve been writing on my &lt;a href="https://github.com/TryGhost/Ghost"&gt;self-hosted Ghost&lt;/a&gt; blog for some time now. In case you’re wondering, this site is hosted on a &lt;a href="https://www.digitalocean.com/?refcode=afdb6bd48884&amp;amp;utm_campaign=Referral_Invite&amp;amp;utm_medium=Referral_Program&amp;amp;utm_source=badge"&gt;Digital Ocean&lt;/a&gt; Droplet.&lt;/p&gt;

&lt;p&gt;For the most part, I felt like I was doing something inconsequential that only meant much for myself. Today, the site has grown to a size that it’d feel like a hat-flying slap to my face if I were to lose all my content.&lt;/p&gt;

&lt;p&gt;If you’re looking for a backup solution for your self-hosted Ghost blog, you’ve come to the right place.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TL;DR: How to automate backup for your self-hosted Ghost blog to cloud storage like Google Drive&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;Getting started with Ghost is easy. You would typically pick between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://ghost.org/pricing/"&gt;Ghost (Pro)&lt;/a&gt; managed service&lt;/li&gt;
&lt;li&gt;  Self-hosted on a &lt;a href="https://www.dreamhost.com/blog/beginners-guide-vps/#what-is-vps"&gt;VPS&lt;/a&gt; or serverless platform like &lt;a href="https://blog.railway.app/p/ghost"&gt;Railway&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’d recommend anyone (especially non-developers) to opt for the managed version.&lt;/p&gt;

&lt;p&gt;Yes, it’s relatively more expensive (so is every managed service). However, it’d most likely save you a bunch of headaches (and time) that come along with self-hosting any other sites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Backups&lt;/li&gt;
&lt;li&gt;  Maintenance&lt;/li&gt;
&lt;li&gt;  Downtime recovery&lt;/li&gt;
&lt;li&gt;  Security, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, you’d sleep better at night.&lt;/p&gt;

&lt;p&gt;On top of that, 100% of the revenue goes to funding the development of the open source project itself — a win-win.&lt;/p&gt;

&lt;h3&gt;
  
  
  “Uh, why are you self-hosting Ghost then?”
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Price — nothing beats the price affordability of hosting on your own dedicated server&lt;/li&gt;
&lt;li&gt;Knowledge gain — I’ve learned a lot from hosting and managing my own VPS&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Other perks of self-hosting include customizability, control, privacy, etc. — which are great, albeit not my primary reasons.&lt;/p&gt;

&lt;p&gt;Most importantly, all the hassles above of self-hosting came to me as fun.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Until it isn’t, I guess.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The pain of backing up Ghost
&lt;/h3&gt;

&lt;p&gt;Setting up Ghost on Digital Ocean is as easy as &lt;a href="https://marketplace.digitalocean.com/apps/ghost"&gt;a click of a button&lt;/a&gt;. Yet, there isn’t any proper in-house solution to back up your Ghost site.&lt;/p&gt;

&lt;p&gt;From &lt;a href="https://ghost.org/docs/faq/manual-backup/"&gt;Ghost’s documentation&lt;/a&gt;, you can manually backup your Ghost site through Ghost Admin. Alternatively, you could use the &lt;code&gt;ghost backup&lt;/code&gt; &lt;a href="https://ghost.org/docs/ghost-cli/#ghost-backup"&gt;command&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Even so, there was no mention of database backup as of the time of writing this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backing up with Bash
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Bash
&lt;/h3&gt;

&lt;p&gt;Simplicity. On top of that, Bash is great for command line interaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are we backing up
&lt;/h3&gt;

&lt;p&gt;Two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Ghost &lt;code&gt;content/&lt;/code&gt; — which includes your site/blog content in JSON, member CSV export, themes, images, and some configuration files&lt;/li&gt;
&lt;li&gt;  MySQL database&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;In this article, we’re going to write a simple Bash script that does all the following steps for us.&lt;/p&gt;

&lt;p&gt;Assuming that we already have &lt;a href="https://rclone.org/"&gt;Rclone&lt;/a&gt; set up, here’s an overview of what our Bash script should cover:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--cBUL3YCe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zy06x5myc8oa7t5w1gcn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cBUL3YCe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zy06x5myc8oa7t5w1gcn.png" alt="Wraith.png" width="800" height="110"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Optional: run requirement checks to ensure that the CLIs that we need are installed. E.g. &lt;code&gt;mysqldump&lt;/code&gt;, &lt;code&gt;rclone&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;Back up the &lt;code&gt;content/&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;Back up our MySQL database&lt;/li&gt;
&lt;li&gt;Copy the backup files over to our cloud storage (e.g. Google Drive) using Rclone&lt;/li&gt;
&lt;li&gt;Optional: clean up the generated backup files&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Utility functions
&lt;/h3&gt;

&lt;p&gt;Let’s create &lt;code&gt;util.sh&lt;/code&gt; which contains a set of helper functions for our backup script.&lt;/p&gt;

&lt;p&gt;Personally, I really like having timestamps printed on my logs, so:&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;#!/bin/bash&lt;/span&gt;

log&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this, we can now use &lt;code&gt;log&lt;/code&gt; instead of &lt;code&gt;echo&lt;/code&gt; to print text; with the timestamp using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ log 'Hola Jerry!'
Sun Jul 22 03:01:52 UTC 2022: Hola Jerry!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we’ll create a utility function that helps to check if a command is installed:&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;# util.sh&lt;/span&gt;

&lt;span class="c"&gt;# ...&lt;/span&gt;

check_command_installation&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;log &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt; is not installed"&lt;/span&gt;
        &lt;span class="nb"&gt;exit &lt;/span&gt;0
    &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;We can use this function in Step 1 to ensure that we have &lt;code&gt;ghost&lt;/code&gt;, &lt;code&gt;mysqldump&lt;/code&gt;, etc. installed before we start our backup process. If the CLI is not installed, we would just log and exit.&lt;/p&gt;

&lt;h3&gt;
  
  
  The backup script
&lt;/h3&gt;

&lt;p&gt;In this section, we’ll create a &lt;code&gt;backup.sh&lt;/code&gt; file as our main backup Bash script.&lt;/p&gt;

&lt;p&gt;To keep our code organized, we break the steps in the overview into individual functions.&lt;/p&gt;

&lt;p&gt;Before we begin, we’ll need to declare some variables and source our &lt;code&gt;util.sh&lt;/code&gt; so that we can use the utility functions that we defined earlier:&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;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nb"&gt;source &lt;/span&gt;util.sh

&lt;span class="nv"&gt;GHOST_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/www/ghost/"&lt;/span&gt;

&lt;span class="nv"&gt;REMOTE_BACKUP_LOCATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ghost_backups/"&lt;/span&gt;

&lt;span class="nv"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y_%m_%d_%H%M&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;GHOST_CONTENT_BACKUP_FILENAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ghost_content_&lt;/span&gt;&lt;span class="nv"&gt;$TIMESTAMP&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz"&lt;/span&gt;
&lt;span class="nv"&gt;GHOST_MYSQL_BACKUP_FILENAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ghost_mysql_&lt;/span&gt;&lt;span class="nv"&gt;$TIMESTAMP&lt;/span&gt;&lt;span class="s2"&gt;.sql.gz"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 1: Run checks&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Check if the default &lt;code&gt;/var/www/ghost&lt;/code&gt; directory exists. &lt;code&gt;ghost&lt;/code&gt; CLI can only be invoked within a folder where Ghost was installed&lt;/li&gt;
&lt;li&gt;  Check if the required CLIs to run our backup are installed
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;span class="c"&gt;# ...&lt;/span&gt;

pre_backup_checks&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GHOST_DIR&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;log &lt;span class="s2"&gt;"Ghost directory does not exist"&lt;/span&gt;
        &lt;span class="nb"&gt;exit &lt;/span&gt;0
    &lt;span class="k"&gt;fi

    &lt;/span&gt;log &lt;span class="s2"&gt;"Running pre-backup checks"&lt;/span&gt;
    &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$GHOST_DIR&lt;/span&gt;

    &lt;span class="nv"&gt;cli&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"tar"&lt;/span&gt; &lt;span class="s2"&gt;"gzip"&lt;/span&gt; &lt;span class="s2"&gt;"mysql"&lt;/span&gt; &lt;span class="s2"&gt;"mysqldump"&lt;/span&gt; &lt;span class="s2"&gt;"ghost"&lt;/span&gt; &lt;span class="s2"&gt;"rclone"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;c &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;cli&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
        &lt;/span&gt;check_command_installation &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Backup the content directory&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Compress the &lt;code&gt;content/&lt;/code&gt; directory into a &lt;code&gt;.gz&lt;/code&gt; file
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;span class="c"&gt;# ...&lt;/span&gt;

backup_ghost_content&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    log &lt;span class="s2"&gt;"Dumping Ghost content..."&lt;/span&gt;
    &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$GHOST_DIR&lt;/span&gt;

    &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-czf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GHOST_CONTENT_BACKUP_FILENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; content/
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Backup MySQL database&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Fetch all the necessary database credentials (username, password, DB name) from the Ghost CLI&lt;/li&gt;
&lt;li&gt;  Run a check to ensure that we are able to connect to our MySQL database using the credentials above&lt;/li&gt;
&lt;li&gt;  Create a MySQL dump and compress it into a &lt;code&gt;.gz&lt;/code&gt; ****file
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;span class="c"&gt;# ...&lt;/span&gt;

check_mysql_connection&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    log &lt;span class="s2"&gt;"Checking MySQL connection..."&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; mysql &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$mysql_user&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$mysql_password&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;";"&lt;/span&gt; &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;log &lt;span class="s2"&gt;"Could not connect to MySQL"&lt;/span&gt;
        &lt;span class="nb"&gt;exit &lt;/span&gt;0
    &lt;span class="k"&gt;fi
    &lt;/span&gt;log &lt;span class="s2"&gt;"MySQL connection OK"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

backup_mysql&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    log &lt;span class="s2"&gt;"Backing up MySQL database"&lt;/span&gt;
    &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$GHOST_DIR&lt;/span&gt;

    &lt;span class="nv"&gt;mysql_user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ghost config get database.connection.user | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;mysql_password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ghost config get database.connection.password | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;mysql_database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ghost config get database.connection.database | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

    check_mysql_connection

    log &lt;span class="s2"&gt;"Dumping MySQL database..."&lt;/span&gt;
    mysqldump &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$mysql_user&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$mysql_password&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$mysql_database&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--no-tablespaces&lt;/span&gt; | &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GHOST_MYSQL_BACKUP_FILENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Copying the compressed backup files to a cloud storage&lt;/strong&gt;&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;# backup.sh&lt;/span&gt;

&lt;span class="c"&gt;# ...&lt;/span&gt;

rclone_to_cloud_storage&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    log &lt;span class="s2"&gt;"Rclone backup..."&lt;/span&gt;
    &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$GHOST_DIR&lt;/span&gt;

    &lt;span class="nv"&gt;rclone_remote_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"remote"&lt;/span&gt;

    rclone copy &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GHOST_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$GHOST_CONTENT_BACKUP_FILENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$rclone_remote_name&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$REMOTE_BACKUP_LOCATION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    rclone copy &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GHOST_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$GHOST_MYSQL_BACKUP_FILENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$rclone_remote_name&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$REMOTE_BACKUP_LOCATION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5: Clean up the backup files&lt;/strong&gt;&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;# backup.sh&lt;/span&gt;

&lt;span class="c"&gt;# ...&lt;/span&gt;

clean_up&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    log &lt;span class="s2"&gt;"Cleaning up old backups..."&lt;/span&gt;
    &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$GHOST_DIR&lt;/span&gt;

    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GHOST_CONTENT_BACKUP_FILENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GHOST_MYSQL_BACKUP_FILENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we shall invoke all of the functions defined for Steps 1 — 5.&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;# At the end of the backup.sh&lt;/span&gt;

&lt;span class="c"&gt;# ...&lt;/span&gt;

log &lt;span class="s2"&gt;"Welcome to Wraith"&lt;/span&gt;
pre_backup_checks
backup_ghost_content
backup_mysql
rclone_to_cloud_storage
clean_up
log &lt;span class="s2"&gt;"Completed backup to &lt;/span&gt;&lt;span class="nv"&gt;$REMOTE_BACKUP_LOCATION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And… we’re done!&lt;/p&gt;

&lt;h3&gt;
  
  
  The final code
&lt;/h3&gt;

&lt;p&gt;You may find the code at &lt;a href="https://github.com/ngshiheng/wraith"&gt;github.com/ngshiheng/wraith&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To use this project directly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SSH into your VPS where you host your Ghost site&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ngshiheng/wraith#set-up-rclone"&gt;Set up Rclone&lt;/a&gt; (important)&lt;/li&gt;
&lt;li&gt;Clone this repository&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;./backup.sh&lt;/code&gt; from the wraith directory&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Automating Backup with Cron
&lt;/h2&gt;

&lt;p&gt;I despise doing manual maintenance and administrative tasks. Let’s schedule a regular backup for our Ghost site to ease our pain using Crontab:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;crontab -e&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;For example, you can run a backup at 5 a.m every Monday with:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# m h  dom mon dow   command&lt;/span&gt;
0 5 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; 1 &lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/backup_script/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./backup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do take timezone into consideration when you set your Cron schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Regardless of the fact that whether you’re just running a simple personal website or a proper business, having a proper backup is critical.&lt;/p&gt;

&lt;p&gt;If a large, well-architected distributed system can go down for days, so can your $5/month Digital Ocean droplet.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>devops</category>
      <category>tooling</category>
    </item>
    <item>
      <title>I Export PostgreSQL Queries to Google Sheets For Free. Here’s How</title>
      <dc:creator>Jerry Ng</dc:creator>
      <pubDate>Tue, 05 Jul 2022 23:58:55 +0000</pubDate>
      <link>https://dev.to/jerrynsh/i-export-postgresql-queries-to-google-sheets-for-free-heres-how-3529</link>
      <guid>https://dev.to/jerrynsh/i-export-postgresql-queries-to-google-sheets-for-free-heres-how-3529</guid>
      <description>&lt;p&gt;A couple of years ago, I wouldn’t have known that connecting a PostgreSQL database to Google Sheets could be this expensive.&lt;br&gt;
Despite being a trivial problem, existing market solutions such as Zapier, KPIBees, etc. require us to pay a premium for it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TL;DR In this article, I am writing about how I was able to export PostgreSQL queries to Google Sheets via GitHub (and a little bit of Bash scripting).&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Did I Need It
&lt;/h2&gt;

&lt;p&gt;Here’s a little bit of context.&lt;/p&gt;

&lt;p&gt;I maintain a tiny side project named &lt;a href="https://github.com/ngshiheng/fareview"&gt;Fareview&lt;/a&gt; — a commercial beer price monitoring tool that scrapes commercial beer data from Singapore e-commerce sites and stores it in a PostgreSQL database.&lt;br&gt;
The summary of the data gathered is then synced daily to &lt;a href="https://docs.google.com/spreadsheets/d/1ImvPhsWp3mRF5lz7C55Ub2Z5okzitIvU6WG77YWL5PU/"&gt;Google Sheets for users to view&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Instead of paying for a monthly premium, I’ve decided to use GitHub Action to help me with such a task for free.&lt;/p&gt;
&lt;h2&gt;
  
  
  Here’s How It Works
&lt;/h2&gt;

&lt;p&gt;This method should also work with any other SQL databases (e.g. MySQL) with a CLI like &lt;code&gt;psql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vmJ-JXK3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k9m0dv5zq7zso948bgcn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vmJ-JXK3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k9m0dv5zq7zso948bgcn.png" alt="A simple illustration of the approach drawn by me" width="800" height="295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a simple Bash script that uses Postgres client CLI (&lt;code&gt;psql&lt;/code&gt;) to run the SQL queries and output them in CSV file format from our PostgreSQL database server&lt;/li&gt;
&lt;li&gt;Set up a GitHub Actions workflow that runs the Bash script in Step 1 and commits the generated file into our repository on a Cron schedule&lt;/li&gt;
&lt;li&gt;On Google Sheets, use the &lt;code&gt;=IMPORTDATA("&amp;lt;url-of-csv-file&amp;gt;")&lt;/code&gt; function to import our CSV data from our repository to our Google Sheets&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It is important to note that the &lt;code&gt;IMPORTDATA&lt;/code&gt; function &lt;a href="https://support.google.com/area120-tables/answer/9904107?hl=en"&gt;updates data automatically at up to 1-hour intervals&lt;/a&gt;. In case you need a shorter interval use case, you may need to &lt;a href="https://gist.github.com/aGHz/6868a1ea1defbd6b9ed5#sheet-scripts"&gt;work around it&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Here Are the Steps To Do It
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Bash Script
&lt;/h3&gt;

&lt;p&gt;Depending on your use case, you may not even need a Bash script. For instance, you could just run the &lt;code&gt;psql&lt;/code&gt; command as one of the steps within your GitHub Actions workflow.&lt;br&gt;
Using a Bash script here provides more flexibility as you could also run this manually outside of GitHub Actions in case you need it.&lt;/p&gt;

&lt;p&gt;This is the Bash script (&lt;code&gt;generate_csv.sh&lt;/code&gt;) that I’m running:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;BRANDS&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"carlsberg"&lt;/span&gt; &lt;span class="s2"&gt;"tiger"&lt;/span&gt; &lt;span class="s2"&gt;"heineken"&lt;/span&gt; &lt;span class="s2"&gt;"guinness"&lt;/span&gt; &lt;span class="s2"&gt;"asahi"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;PGDATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PGDATABASE&lt;/span&gt;&lt;span class="p"&gt;-fareview&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;PGHOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PGHOST&lt;/span&gt;&lt;span class="p"&gt;-localhost&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;PGPORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PGPORT&lt;/span&gt;&lt;span class="p"&gt;-5432&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;PGUSER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PGUSER&lt;/span&gt;&lt;span class="p"&gt;-postgres&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; data/
&lt;span class="k"&gt;for &lt;/span&gt;brand &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BRANDS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PGPASSWORD&lt;/span&gt; psql &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;brand&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"'&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;brand&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGHOST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGUSER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--pset&lt;/span&gt; footer &lt;span class="nt"&gt;-f&lt;/span&gt; alembic/examples/get_all_by_brand.sql &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"data/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;brand&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.csv"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  A simple script uses the &lt;code&gt;psql&lt;/code&gt; command to run SQL query from a &lt;code&gt;.sql&lt;/code&gt; file with CSV table output mode (&lt;code&gt;-A&lt;/code&gt; flag)&lt;/li&gt;
&lt;li&gt;  The output of this command is saved in a CSV file within the &lt;code&gt;data&lt;/code&gt; directory of the Git repository&lt;/li&gt;
&lt;li&gt;  The script gets all the necessary database settings from the environment variables&lt;/li&gt;
&lt;li&gt;  In our GitHub Actions, we’re going to set these environment variables from our repository secrets (note: we’ll have to add these environment variables into our repository ourselves)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s the &lt;a href="https://github.com/ngshiheng/fareview/blob/0bce19b570408f5f8af2a26ed940fe09b0c0bf6c/scripts/generate_csv.sh"&gt;permalink&lt;/a&gt; to the Bash script.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Actions Workflow
&lt;/h3&gt;

&lt;p&gt;Why GitHub Actions?&lt;/p&gt;

&lt;p&gt;GitHub Actions workflow supports running on a &lt;a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule"&gt;Cron schedule&lt;/a&gt;. Essentially, what this means is that we can schedule our job (i.e. our script in this case) to run as short as 5 minutes intervals.&lt;/p&gt;

&lt;p&gt;In our use case, we can use this to export our PostgreSQL query to our Google Sheets daily.&lt;/p&gt;

&lt;p&gt;Let’s start by creating a &lt;code&gt;generate_csv.yml&lt;/code&gt; workflow file inside the &lt;code&gt;.github/workflows&lt;/code&gt; folder of our project directory:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate CSV&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;23&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt; &lt;span class="c1"&gt;# At 23:30 UTC daily&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch"&gt;&lt;code&gt;workflow_dispatch&lt;/code&gt;&lt;/a&gt; is added so that we can manually trigger our workflow from GitHub API, CLI, or browser UI&lt;/li&gt;
&lt;li&gt;  Check out &lt;a href="https://crontab.guru/"&gt;Crontab Guru&lt;/a&gt; for Cron schedule syntax&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, to connect to any database, we’ll need to pass the connection settings:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate CSV&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;23&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;https://crontab.guru/#30_23_*_*_*&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;PGDATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PGDATABASE }}&lt;/span&gt;
    &lt;span class="na"&gt;PGHOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PGHOST }}&lt;/span&gt;
    &lt;span class="na"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PGPASSWORD }}&lt;/span&gt;
    &lt;span class="na"&gt;PGPORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PGPORT }}&lt;/span&gt;
    &lt;span class="na"&gt;PGUSER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PGUSER }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the same GitHub project repository Secrets setting, enter the respective environment variables under Actions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PfS0Ng5p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c0gjp8nv2xy9or217zgw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PfS0Ng5p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c0gjp8nv2xy9or217zgw.png" alt="At https://github.com/&amp;lt;username&amp;gt;/&amp;lt;project&amp;gt;/settings/secrets/actions" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, let's create the job for us to export our PostgreSQL queries and commit them to our repository&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate CSV&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;23&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;https://crontab.guru/#30_23_*_*_*&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;PGDATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PGDATABASE }}&lt;/span&gt;
    &lt;span class="na"&gt;PGHOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PGHOST }}&lt;/span&gt;
    &lt;span class="na"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PGPASSWORD }}&lt;/span&gt;
    &lt;span class="na"&gt;PGPORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PGPORT }}&lt;/span&gt;
    &lt;span class="na"&gt;PGUSER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PGUSER }}&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;generate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
        &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&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;Install PostgreSQL&lt;/span&gt; &lt;span class="c1"&gt;# Step 1&lt;/span&gt;
              &lt;span class="na"&gt;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;sudo apt-get update&lt;/span&gt;
                  &lt;span class="s"&gt;sudo apt-get install --yes postgresql&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;Generate CSV&lt;/span&gt; &lt;span class="c1"&gt;# Step 2&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scripts/generate_csv.sh&lt;/span&gt;
              &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&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;Get current date&lt;/span&gt; &lt;span class="c1"&gt;# Step 3&lt;/span&gt;
              &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;date&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "::set-output name=date::$(TZ=Asia/Singapore date +'%d %b %Y')"&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;Commit changes&lt;/span&gt; &lt;span class="c1"&gt;# Step 4&lt;/span&gt;
              &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ success() }}&lt;/span&gt;
              &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EndBug/add-and-commit@v9&lt;/span&gt;
              &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;pull&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--rebase&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--autostash&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;."&lt;/span&gt;
                  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chore(data):&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;update&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;generated&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;csv&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;automatically&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;steps.date.outputs.date&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;data"&lt;/span&gt;
                  &lt;span class="na"&gt;default_author&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github_actions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A job in a GitHub Actions workflow can contain many steps. Different steps in GitHub Actions run in different containers as well.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first step is to set up and install PostgreSQL (with &lt;code&gt;psql&lt;/code&gt;) onto the step container&lt;/li&gt;
&lt;li&gt;Next, we’ll add a step to run our &lt;a href="https://github.com/ngshiheng/fareview/blob/0bce19b570408f5f8af2a26ed940fe09b0c0bf6c/scripts/generate_csv.sh"&gt;Bash script&lt;/a&gt; that runs an &lt;a href="https://github.com/ngshiheng/fareview/blob/0bce19b570408f5f8af2a26ed940fe09b0c0bf6c/alembic/examples/get_all_by_brand.sql"&gt;SQL query from a file&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Optional: Get the local date-time so that we can use it as part of our commit message in Step 4&lt;/li&gt;
&lt;li&gt;Commit the generated CSV file from Step 2 into our repository. Here, I am using the &lt;a href="https://github.com/marketplace/actions/add-commit"&gt;Add &amp;amp; Commit GitHub Actions&lt;/a&gt; to commit my CSV file changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why use the &lt;code&gt;--autostash&lt;/code&gt; flag with the &lt;code&gt;git pull&lt;/code&gt;? This allows us to automatically stash and pop the pending CSV file changes before committing and pushing them to the repository. This helps us to work around Git commit issues whereby other developers could be pushing new code changes while this job runs.&lt;/p&gt;

&lt;p&gt;That's it! we now have a Cron job that runs every day to update our CSV file for us so that &lt;a href="https://docs.google.com/spreadsheets/d/1ImvPhsWp3mRF5lz7C55Ub2Z5okzitIvU6WG77YWL5PU/"&gt;our Google Sheets&lt;/a&gt; can import them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ueju6j0l--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ilw7phpfmnv79bw3rc2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ueju6j0l--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ilw7phpfmnv79bw3rc2.png" alt="Git commit history of this job" width="772" height="645"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Having GitHub — a highly available service to host our CSV file for us feels great. What’s more, having GitHub hosting this for free almost feels like some sort of a cheat.&lt;/p&gt;

&lt;p&gt;I have also used this similar approach to run a &lt;a href="https://github.com/ngshiheng/michelin-my-maps/blob/95d8a05e7c4393af6ed2f8e380c44ac7cc2e92ba/.github/workflows/scrape.yml"&gt;scraper job to fetch Michelin Guide restaurants&lt;/a&gt; from the Michelin Guide website.&lt;/p&gt;

&lt;p&gt;Alternatively, I have also considered using Google Sheets API to sync my data directly to Google Sheets. Given the integration efforts required, I’m glad that I stick to this very simple method.&lt;/p&gt;

&lt;p&gt;Thanks for reading, and here's the final &lt;a href="https://github.com/ngshiheng/fareview"&gt;link to the example project&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://jerrynsh.com/i-export-postgresql-queries-to-gsheets-for-free-heres-how/"&gt;jerrynsh.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>productivity</category>
      <category>devops</category>
      <category>postgres</category>
    </item>
    <item>
      <title>How I Setup CI/CD Pipeline For Cloudflare Worker</title>
      <dc:creator>Jerry Ng</dc:creator>
      <pubDate>Wed, 01 Jun 2022 00:09:26 +0000</pubDate>
      <link>https://dev.to/jerrynsh/how-i-setup-cicd-pipeline-for-cloudflare-worker-2kha</link>
      <guid>https://dev.to/jerrynsh/how-i-setup-cicd-pipeline-for-cloudflare-worker-2kha</guid>
      <description>&lt;p&gt;Today, most of my projects on GitHub are kept up to date with &lt;a href="https://github.com/renovatebot/renovate" rel="noopener noreferrer"&gt;Renovate&lt;/a&gt;. With auto-merging enabled, I want to have enough confidence that the automated dependencies update would not cause any regressions.&lt;/p&gt;

&lt;p&gt;Testing Cloudflare Worker is a bit out of whack. Don’t get me wrong, I love Cloudflare Worker. However, the &lt;a href="https://blog.cloudflare.com/unit-testing-workers-in-cloudflare-workers/" rel="noopener noreferrer"&gt;existing solution&lt;/a&gt; out there feels a little bit unintuitive. On top of that, &lt;a href="https://github.com/dollarshaveclub/cloudworker" rel="noopener noreferrer"&gt;dollarshaveclub/cloudworker&lt;/a&gt; is no longer actively maintained, which is a bummer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TL;DR: This post talks about how I set up a CI/CD pipeline for a Cloudflare Worker project using GitHub Action and &lt;a href="https://k6.io/" rel="noopener noreferrer"&gt;Grafana k6&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




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

&lt;p&gt;Previously, &lt;a href="https://jerrynsh.com/i-built-my-own-tiny-url/" rel="noopener noreferrer"&gt;I built a URL shortener clone&lt;/a&gt; with Cloudflare Worker. Using this existing project (&lt;a href="https://github.com/ngshiheng/atomic-url" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; link), we shall look into setting up a CI/CD pipeline for it along with simple integration tests.&lt;/p&gt;

&lt;p&gt;Our GitHub Action CI/CD pipeline is rather straightforward. The stages (jobs) are as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Ftlx6zq6yn5d4m4s8w80i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Ftlx6zq6yn5d4m4s8w80i.png" alt="Example CI workflow on GitHub"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example CI workflow on GitHub&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lint check or unit testing&lt;/li&gt;
&lt;li&gt;Deploy to the Staging environment&lt;/li&gt;
&lt;li&gt;Run the integration tests on our Staging environment&lt;/li&gt;
&lt;li&gt;Run &lt;a href="https://github.com/semantic-release/semantic-release" rel="noopener noreferrer"&gt;semantic release&lt;/a&gt; and deploy to the Production environment&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrangler Config
&lt;/h2&gt;

&lt;p&gt;To start, we’ll have to modify our &lt;a href="https://github.com/ngshiheng/atomic-url/blob/v1.1.8/wrangler.toml" rel="noopener noreferrer"&gt;existing &lt;code&gt;wrangler.toml&lt;/code&gt; file&lt;/a&gt;. Remember, we need a Staging environment to deploy to run our integration test against it:&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="nn"&gt;[env.staging]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"atomic-url-staging"&lt;/span&gt;
&lt;span class="py"&gt;workers_dev&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;kv_namespaces&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="err"&gt;{&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;"URL_DB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ca7936b380a840908c035a88d1e76584"&lt;/span&gt; &lt;span class="err"&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;Here are the things to take note of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;name&lt;/code&gt; — Make sure that the &lt;code&gt;name&lt;/code&gt; has to be unique and alphanumerical (&lt;code&gt;-&lt;/code&gt; are allowed) for each environment. Let’s name our Staging environment &lt;code&gt;atomic-url-staging&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;worker_dev&lt;/code&gt; — Our integration tests run on &lt;code&gt;&amp;lt;NAME&amp;gt;.&amp;lt;SUBDOMAIN&amp;gt;.workers.dev&lt;/code&gt; endpoint. Hence, we need to deploy our Cloudflare Worker by setting &lt;code&gt;workers_dev = true&lt;/code&gt; (&lt;a href="https://developers.cloudflare.com/workers/platform/environments/#publishing-to-workersdev" rel="noopener noreferrer"&gt;reference&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;kv_namespaces&lt;/code&gt; — Atomic URL uses KV as its database to store shortened URLs. Here, I chose to use a preview namespace KV as the test database. Why? Simply because it is the same development KV namespace that I use during local development (when running &lt;code&gt;wrangler dev&lt;/code&gt;). Of course, you could just use a regular KV namespace. Just make sure that you’re not using Production KV &lt;code&gt;id&lt;/code&gt;. &lt;a href="https://developers.cloudflare.com/workers/wrangler/cli-wrangler/commands/#kv_namespaces" rel="noopener noreferrer"&gt;Read how to create a KV namespace&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, we’ll also need to create a Production environment to deploy to with our production KV namespace:&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="nn"&gt;[env.production]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"atomic-url"&lt;/span&gt;
&lt;span class="py"&gt;route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"s.jerrynsh.com/*"&lt;/span&gt;
&lt;span class="py"&gt;workers_dev&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;kv_namespaces&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="err"&gt;{&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;"URL_DB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"7da8f192d2c1443a8b2ca76b22a8069f"&lt;/span&gt; &lt;span class="err"&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 Production environment section would be similar to before except — we’ll be setting &lt;code&gt;worker_dev = false&lt;/code&gt; and &lt;code&gt;route&lt;/code&gt; for production.&lt;/p&gt;

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

&lt;p&gt;To deploy manually from your local machine to their respective environment, run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;wrangler publish -e staging&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;wrangler publish -e production&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Though, we’ll look into how to do this automatically via our CI/CD pipeline using GitHub Actions.&lt;/p&gt;

&lt;p&gt;Before we move on, you may find the complete &lt;code&gt;wrangler.toml&lt;/code&gt; configuration &lt;a href="https://github.com/ngshiheng/atomic-url/blob/f1ff25a00c1430314b45dde855497b58831b8e3a/wrangler.toml" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Oh, here’s a &lt;a href="https://developers.cloudflare.com/workers/wrangler/configuration/#configure-wranglertoml" rel="noopener noreferrer"&gt;cheat sheet&lt;/a&gt; to configure &lt;code&gt;wrangler.toml&lt;/code&gt;. I highly recommend you make use of this!&lt;/p&gt;

&lt;h2&gt;
  
  
  Grafana k6
&lt;/h2&gt;

&lt;p&gt;For integration testing, we’ll be using a tool known as k6. Generally, k6 is used as a tool for performance and load testing. Now, bear with me, we’re not going to integrate any load tests into our CI/CD pipeline; not today.&lt;/p&gt;

&lt;p&gt;Here, we’ll be running &lt;a href="https://k6.io/docs/test-types/smoke-testing/" rel="noopener noreferrer"&gt;smoke tests&lt;/a&gt; for this project whenever new commits are pushed to our &lt;code&gt;main&lt;/code&gt; branch. A smoke test is essentially a type of integration test that performs a sanity check on a system.&lt;/p&gt;

&lt;p&gt;In this case, running a smoke test is sufficient enough for me to determine that our system is deployed without any regression and can run on minimal load.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to test
&lt;/h3&gt;

&lt;p&gt;Basically, here are a couple of things that we want to check as part of our smoke test for our URL shortener app in &lt;a href="https://k6.io/docs/using-k6/tags-and-groups/#groups" rel="noopener noreferrer"&gt;groups&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The main page should load as expected with a 200 response status
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visit main page&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&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="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&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="s2"&gt;is status 200&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;r&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verify homepage text&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;r&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;r&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="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;A URL shortener POC built using Cloudflare Worker&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  Our main POST API endpoint &lt;code&gt;/api/url&lt;/code&gt; should create a short URL with the original URL
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visit rest endpoint&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/url`&lt;/span&gt;&lt;span class="p"&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;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;originalUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DUMMY_ORIGINAL_URL&lt;/span&gt; &lt;span class="p"&gt;}),&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="s2"&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="s2"&gt;application/json&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="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&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="s2"&gt;is status 200&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;r&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verify createShortUrl&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;r&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;urlKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shortUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;originalUrl&lt;/span&gt; &lt;span class="p"&gt;}&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;r&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="nx"&gt;shortenLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;shortUrl&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;urlKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;originalUrl&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;DUMMY_ORIGINAL_URL&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;ul&gt;
&lt;li&gt;  Make sure that when we visit the generated short URL, it redirects us to the original URL
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visit shortUrl&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&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="nx"&gt;shortenLink&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&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="s2"&gt;is status 200&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;r&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verify original url&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;r&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;r&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;DUMMY_ORIGINAL_URL&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;Finally, we configure out &lt;code&gt;BASE_URL&lt;/code&gt; to &lt;a href="https://github.com/ngshiheng/atomic-url/blob/f1ff25a00c1430314b45dde855497b58831b8e3a/scripts/test.js#L15" rel="noopener noreferrer"&gt;point to our newly created Staging environment&lt;/a&gt; in the previous section.&lt;/p&gt;

&lt;p&gt;To test locally, simply run &lt;code&gt;k6 path/to/test.js&lt;/code&gt;. That’s all! You may find the &lt;a href="https://github.com/ngshiheng/atomic-url/blob/f1ff25a00c1430314b45dde855497b58831b8e3a/scripts/test.js" rel="noopener noreferrer"&gt;full test script here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In case you are thinking about running load tests, read &lt;a href="https://k6.io/blog/monthly-visits-concurrent-users/" rel="noopener noreferrer"&gt;how to determine concurrent users&lt;/a&gt; in your load test.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions
&lt;/h2&gt;

&lt;p&gt;I’ll be glossing over this section as it is pretty straightforward. You may refer to the final &lt;a href="https://github.com/ngshiheng/atomic-url/blob/f1ff25a00c1430314b45dde855497b58831b8e3a/.github/workflows/ci.yml" rel="noopener noreferrer"&gt;GitHub Actions workflow file here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s piece together everything we have. Below are the GitHub Actions that we’ll need to use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://github.com/cloudflare/wrangler-action/tree/1.3.0" rel="noopener noreferrer"&gt;wrangler-action&lt;/a&gt; — For deploying Cloudflare Worker using Wrangler CLI&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://github.com/marketplace/actions/k6-load-test?version=v0.2.0" rel="noopener noreferrer"&gt;k6-action&lt;/a&gt; — For running k6&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing to note about the workflow file — to make a job depend (need) on another job, we’ll make use of the &lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds" rel="noopener noreferrer"&gt;&lt;code&gt;need syntax&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Actions Secrets
&lt;/h3&gt;

&lt;p&gt;This project requires 2 Actions secrets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;CF_API_TOKEN&lt;/code&gt; — To be used by Wrangler GitHub Action to automatically publish our Cloudflare Worker to its respective environment. You can &lt;a href="https://dash.cloudflare.com/profile/api-tokens" rel="noopener noreferrer"&gt;create your API token&lt;/a&gt; using the &lt;code&gt;Edit Cloudflare Workers&lt;/code&gt; template.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NPM_TOKEN&lt;/code&gt; — This project also uses &lt;a href="https://github.com/semantic-release/npm" rel="noopener noreferrer"&gt;semantic-release&lt;/a&gt; to automatically publish to &lt;a href="https://www.npmjs.com/" rel="noopener noreferrer"&gt;NPM&lt;/a&gt;. To enable this, you will need to create an &lt;code&gt;NPM_TOKEN&lt;/code&gt; via &lt;a href="https://docs.npmjs.com/getting-started/working_with_tokens#how-to-create-new-tokens" rel="noopener noreferrer"&gt;npm create token&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To add it to your GitHub repository secrets, check out &lt;a href="https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository" rel="noopener noreferrer"&gt;this guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you had taken a look at the final workflow file, you may have noticed the syntax &lt;code&gt;${{ secrets.GITHUB_TOKEN }}&lt;/code&gt; and wondered why I didn’t mention anything about adding &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; to our project Actions secrets. Turns out, it is &lt;a href="https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret" rel="noopener noreferrer"&gt;automatically created and added to all of your workflows&lt;/a&gt;.&lt;/p&gt;




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

&lt;p&gt;Understandably, serverless platforms are generally known to be hard to test and debug. However, that doesn’t mean that we should ignore it.&lt;/p&gt;

&lt;p&gt;So, what’s next? Right on top of my head, we could do better. Here are a couple of improvements that we can make:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a job/stage that automatically rolls back and reverts commit when the smoke tests fail&lt;/li&gt;
&lt;li&gt;Create an individual testing environment upon PR creation so that we can run smoke tests on them&lt;/li&gt;
&lt;li&gt;Probably overkill for this project — implementing &lt;a href="https://octopus.com/docs/deployments/patterns/canary-deployments" rel="noopener noreferrer"&gt;canary deployment&lt;/a&gt; sounds like a good challenge&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;If you’re looking into unit testing Cloudflare Workers, here are my recommendations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://findwork.dev/blog/testing-cloudflare-workers/" rel="noopener noreferrer"&gt;https://findwork.dev/blog/testing-cloudflare-workers/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://blog.cloudflare.com/unit-testing-workers-in-cloudflare-workers/" rel="noopener noreferrer"&gt;https://blog.cloudflare.com/unit-testing-workers-in-cloudflare-workers/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://blog.cloudflare.com/unit-testing-worker-functions/" rel="noopener noreferrer"&gt;https://blog.cloudflare.com/unit-testing-worker-functions/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a decent video about setting up an ideal yet practical CI/CD pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://www.youtube.com/watch?v=OPwU3UWCxhw" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=OPwU3UWCxhw&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://jerrynsh.com/how-i-setup-ci-cd-pipeline-for-cloudflare-worker/" rel="noopener noreferrer"&gt;jerrynsh.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>serverless</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Saying Goodbye to Heroku Postgres for Now</title>
      <dc:creator>Jerry Ng</dc:creator>
      <pubDate>Thu, 28 Apr 2022 14:39:11 +0000</pubDate>
      <link>https://dev.to/jerrynsh/saying-goodbye-to-heroku-postgres-for-now-3p5g</link>
      <guid>https://dev.to/jerrynsh/saying-goodbye-to-heroku-postgres-for-now-3p5g</guid>
      <description>&lt;p&gt;A little less than a year ago, I built &lt;a href="https://burplist.me/" rel="noopener noreferrer"&gt;Burplist&lt;/a&gt;, a free search engine for craft beer in Singapore. With the goal to &lt;a href="https://jerrynsh.com/how-i-built-burplist-for-free/" rel="noopener noreferrer"&gt;keep my infrastructure cost as low as possible&lt;/a&gt;, I started off with &lt;a href="https://elements.heroku.com/addons/heroku-postgresql" rel="noopener noreferrer"&gt;Heroku Postgres free tier&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With a row limit of 10,000 and a storage capacity of 1 GB, I thought that it would last me for at least a year — it didn’t.&lt;/p&gt;

&lt;p&gt;Despite having a garbage collector service that runs on a weekly basis to remove staled rows, Heroku Postgres’s free tier simply wasn’t enough. I started looking for alternatives.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TL;DR: I’ve migrated my side projects’ PostgreSQL to &lt;a href="https://railway.app?referralCode=jerrynsh" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; because of its pricing and ease of migration.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Heroku Postgres Alternatives
&lt;/h2&gt;

&lt;p&gt;After some Googling, I came across a couple of PaaS similar to Heroku, each with its own Postgres offerings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://render.com/pricing" rel="noopener noreferrer"&gt;render.com/pricing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://fly.io/docs/about/pricing/#postgresql-clusters" rel="noopener noreferrer"&gt;fly.io/docs/about/pricing/#postgresql-clusters&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://railway.app/pricing" rel="noopener noreferrer"&gt;railway.app/pricing&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  One stood out
&lt;/h3&gt;

&lt;p&gt;I chose Railway because it makes the most economical sense for my use case. Below are some of the things that I like about Railway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://railway.app/pricing" rel="noopener noreferrer"&gt;Incredibly generous pricing&lt;/a&gt;, where you would only start paying for resources usage after $10. In my case, this is a lot cheaper than using Heroku.&lt;/li&gt;
&lt;li&gt;  They have a &lt;a href="https://railway.app/help" rel="noopener noreferrer"&gt;Discord community&lt;/a&gt; where you can easily get help from.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rather slick and intuitive UI. On top of that, you can view and make SQL queries directly via the dashboard. Though, I would argue that using a database tool like &lt;a href="https://tableplus.com/" rel="noopener noreferrer"&gt;TablePlus&lt;/a&gt; or &lt;a href="https://www.pgadmin.org/" rel="noopener noreferrer"&gt;pgAdmin&lt;/a&gt; would be much more convenient.&lt;br&gt;
&lt;a href="https://media.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%2Fz3ykmoxknbhuegqn7pbq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fz3ykmoxknbhuegqn7pbq.png" alt="You can only view and make SQL queries on the default database name  raw `railway` endraw "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can only view and make SQL queries on the default database name &lt;code&gt;railway&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The dashboard also provides CPU, memory, and network metrics on your database usage; which is something that is unavailable on Heroku’s free tier.&lt;br&gt;
&lt;a href="https://media.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%2Fdejahzne6ztzw5ji0aca.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fdejahzne6ztzw5ji0aca.png" alt="Railway PostgreSQL dashboard metrics"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Railway PostgreSQL dashboard metrics&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Migrate
&lt;/h2&gt;

&lt;p&gt;The migration was a piece of cake. This is easily one of the push factors that made the decision of migrating to Railway Postgres.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-requisite
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Installed &lt;code&gt;postgresql&lt;/code&gt; locally on your machine. For e.g. if you’re on Mac — &lt;code&gt;brew install postgresql&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Make sure you can run the &lt;code&gt;[pg_restore](http://www.postgresql.org/docs/current/static/app-pgrestore.html)&lt;/code&gt; command&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Setup Railway
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Setup a Railway account (&lt;a href="https://railway.app?referralCode=jerrynsh" rel="noopener noreferrer"&gt;referral link&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Go to your Railway dashboard and create a new project and provision PostgreSQL
&lt;img src="https://media.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%2Fr8mw1wgkumwhe9a5bmst.png" alt="Take note of your database credentials under the "&gt;
&amp;gt; Take note of your database credentials under the "Connect" or "Variables" tab. You'll need them later&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  5 Migration steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Export Heroku Postgres (&lt;a href="https://devcenter.heroku.com/articles/heroku-postgres-import-export" rel="noopener noreferrer"&gt;reference&lt;/a&gt;).
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;heroku pg:backups:capture &lt;span class="nt"&gt;-a&lt;/span&gt; &amp;lt;heroku_app_name&amp;gt;
heroku pg:backups:download &lt;span class="nt"&gt;-a&lt;/span&gt; &amp;lt;heroku_app_name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;At your current working directory, you should see &lt;code&gt;latest.dump&lt;/code&gt; file
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;heroku pg:backups:capture &lt;span class="nt"&gt;-a&lt;/span&gt; &amp;lt;heroku_app_name&amp;gt;

Starting backup of postgresql-encircled-90125... &lt;span class="k"&gt;done
&lt;/span&gt;Use Ctrl-C at any &lt;span class="nb"&gt;time &lt;/span&gt;to stop monitoring progress&lt;span class="p"&gt;;&lt;/span&gt; the backup will &lt;span class="k"&gt;continue &lt;/span&gt;running.
Use heroku pg:backups:info to check progress.
Stop a running backup with heroku pg:backups:cancel.
Backing up DATABASE to b001... &lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;heroku pg:backups:download &lt;span class="nt"&gt;-a&lt;/span&gt; &amp;lt;heroku_app_name&amp;gt;
Getting backup from ⬢ &amp;lt;heroku_app_name&amp;gt;... &lt;span class="k"&gt;done&lt;/span&gt;, &lt;span class="c"&gt;#1&lt;/span&gt;
Downloading latest.dump... ████████████████████████▏  100% 00:00 309.99KB

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls
&lt;/span&gt;latest.dump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;In the same working directory with &lt;code&gt;latest.dump&lt;/code&gt;, run the following command to import your downloaded database dump to your Railway Postgres. Update accordingly with your own Postgres credentials:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# NOTE:&lt;/span&gt;
&lt;span class="c"&gt;# Keep PGDATABASE as `railway` if you want to view your tables via the Railway dashboard.&lt;/span&gt;
&lt;span class="c"&gt;# If you insist on another database name, you will have to create your own database via psql command.&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# E.g.:&lt;/span&gt;
&lt;span class="c"&gt;# PGPASSWORD=$PGPASSWORD psql -h $PGHOST -U $PGUSER -p $PGPORT -d $PGDATABASE&lt;/span&gt;
&lt;span class="c"&gt;# CREATE DATABSE your_database_name&lt;/span&gt;

&lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PGPASSWORD&lt;/span&gt; pg_restore &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nv"&gt;$PGHOST&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="nv"&gt;$PGUSER&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;$PGPORT&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt; &amp;lt; latest.dump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Verify that your data is imported correctly. To do so, you can use the &lt;code&gt;psql&lt;/code&gt; command as shown in the example in Step 4; or connect to your own database via a tool like pgAdmin.&lt;/li&gt;
&lt;li&gt;Go to your existing apps, e.g. your Heroku app, and update the database connection URL/credentials accordingly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s it!&lt;/p&gt;

&lt;h2&gt;
  
  
  I Miss Heroku Dataclip
&lt;/h2&gt;

&lt;p&gt;One thing that I really miss about Heroku Postgres is its ability to share query results with &lt;a href="https://devcenter.heroku.com/articles/dataclips" rel="noopener noreferrer"&gt;Dataclips&lt;/a&gt;. Heroku Dataclips is incredibly useful as you can easily:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make a SQL query via the UI&lt;/li&gt;
&lt;li&gt;Share the output in JSON or CSV format via a link&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you have a sharable link to CSV, you easily import it to other SaaS such as Google Sheets. On top of that, most data processing SaaS supports CSV out of the box; this makes Dataclip incredibly useful.&lt;/p&gt;

&lt;p&gt;Now, I’ll either have to generate my own CSV on another server or pray that the SaaS that I’m using has PostgreSQL integration (hint: most don’t).&lt;/p&gt;

&lt;p&gt;Having this said, I am still more than happy to make this tradeoff.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Uses
&lt;/h3&gt;

&lt;p&gt;Besides using Railway as my PostgreSQL database server for my projects, I am also using it to host my own &lt;a href="https://umami.is/" rel="noopener noreferrer"&gt;umami&lt;/a&gt; analytics site in combination with &lt;a href="https://vercel.com/" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’d also recommend you to check out their other offerings such as Redis, MongoDB, and also their starter project templates.&lt;/p&gt;

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

&lt;p&gt;No, I’m not leaving Heroku entirely. Heroku has stood the test of time. I would expect a company owned by Salesforce to provide better reliability and stability.&lt;/p&gt;

&lt;p&gt;On top of that Heroku’s &lt;a href="https://elements.heroku.com/addons" rel="noopener noreferrer"&gt;add-ons&lt;/a&gt; often come in handy. Add-ons provide useful integration out of the box e.g. logging, search, and many more!&lt;/p&gt;

&lt;p&gt;On the contrary, I can’t speak for the reliability and uptime of the Railway. Having that said, the projects that I am running allow me to take on this level of risk, and my experience so far has been great.&lt;/p&gt;

&lt;p&gt;I’m rooting for &lt;a href="https://railway.app?referralCode=jerrynsh" rel="noopener noreferrer"&gt;Railway&lt;/a&gt;. I simply enjoy seeing competitions in the market.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://jerrynsh.com/saying-goodbye-to-heroku-postgres/" rel="noopener noreferrer"&gt;jerrynsh.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>database</category>
      <category>postgres</category>
      <category>heroku</category>
    </item>
    <item>
      <title>12 Tips to Self-host Renovate Bot</title>
      <dc:creator>Jerry Ng</dc:creator>
      <pubDate>Mon, 04 Apr 2022 00:38:03 +0000</pubDate>
      <link>https://dev.to/jerrynsh/12-tips-to-self-host-renovate-bot-446l</link>
      <guid>https://dev.to/jerrynsh/12-tips-to-self-host-renovate-bot-446l</guid>
      <description>&lt;p&gt;Updating dependencies is boring. Despite its importance, we always find excuses to avoid updating them in the phrase of “if it ain't broke, don't fix it” or “there are more important features to work on”. Over time, the maintainability of the projects deteriorates. The team ends up with a 3-year-old dependency where no one is brave enough to bump it.&lt;/p&gt;

&lt;p&gt;So, what’s the solution? Let the robots do our boring job!&lt;/p&gt;

&lt;p&gt;In this article, you will learn tips on running a self-hosted &lt;a href="https://github.com/renovatebot/renovate"&gt;Renovate&lt;/a&gt; bot with GitLab as an example. If you’re looking for a guide on how to start using Renovate on GitHub, I’d highly recommend you to &lt;a href="https://www.freecodecamp.org/news/update-dependencies-automatically-with-github-actions-and-renovate/#Getting-Started"&gt;read this&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why use Renovate&lt;/li&gt;
&lt;li&gt;Getting started on self-hosted Renovate (GitLab as an example)&lt;/li&gt;
&lt;li&gt;How to run Renovate locally&lt;/li&gt;
&lt;li&gt;Debugging Renovate jobs&lt;/li&gt;
&lt;li&gt;12 useful Renovate tips&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disclaimer: these tips are my own opinions from lessons gathered from hours working. Enjoy!&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Use Renovate
&lt;/h2&gt;

&lt;p&gt;In short, Renovate (&lt;a href="https://docs.renovatebot.com/"&gt;official doc&lt;/a&gt;) helps to update project dependencies (private or third-party) automatically. &lt;/p&gt;

&lt;p&gt;How? Renovate parses our projects’ dependency management files such as &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;go.mod&lt;/code&gt; file and raises a pull/merge request (PR/MR) with the updated dependencies accordingly.&lt;/p&gt;

&lt;p&gt;Renovate is highly customizable via a simple configuration file (&lt;code&gt;config.js&lt;/code&gt;). With rather intuitive configuration settings, it also supports a &lt;a href="https://docs.renovatebot.com/modules/manager/#supported-managers"&gt;wide range of package managers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Today, Renovate supports many other Git platforms such as Bitbucket, Gitea, or even Azure DevOps. On top of that, it’s &lt;a href="https://github.com/renovatebot/renovate#who-uses-renovate"&gt;used by a lot of popular development communities or companies&lt;/a&gt; such as Uber, GitLab, Prisma, Netlify, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  Highlight
&lt;/h3&gt;

&lt;p&gt;The best part about Renovate is its ability to auto-merge PRs/MRs. &lt;/p&gt;

&lt;p&gt;That aside, the flexibility provided by the &lt;code&gt;[packageRules&lt;/code&gt; feature](&lt;a href="https://docs.renovatebot.com/configuration-options/#packagerules"&gt;https://docs.renovatebot.com/configuration-options/#packagerules&lt;/a&gt;) which is used to apply rules to specific dependencies (in individual or group) is incredibly handy.&lt;/p&gt;

&lt;h2&gt;
  
  
  How To Run Self-hosted Renovate on GitLab
&lt;/h2&gt;

&lt;p&gt;To run Renovate on self-hosted GitLab, you’ll need a private GitLab project (&lt;a href="https://about.gitlab.com/blog/2016/01/27/comparing-terms-gitlab-github-bitbucket/"&gt;i.e. repository&lt;/a&gt; in the following). This bot repository will be used to host &lt;a href="https://docs.gitlab.com/runner/"&gt;GitLab runners&lt;/a&gt; which run Renovate for your other projects.&lt;/p&gt;

&lt;p&gt;Next, assuming that you already have GitLab runners installed and set up, you’ll need the following in the bot project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure the following CI/CD variables:

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;RENOVATE_TOKEN&lt;/code&gt; — GitLab &lt;a href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#creating-a-personal-access-token"&gt;Personal Access Token&lt;/a&gt; (PAT) with scopes: &lt;code&gt;read_user&lt;/code&gt;, &lt;code&gt;api&lt;/code&gt;, and &lt;code&gt;write_repository&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GITHUB_COM_TOKEN&lt;/code&gt; —  GitHub PAT with minimum scope.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;
&lt;li&gt;

&lt;p&gt;&lt;code&gt;config.js&lt;/code&gt; (NOTE: here’s a &lt;a href="https://gitlab.com/gitlab-org/frontend/renovate-gitlab-bot/-/blob/main/renovate/config.js"&gt;more complex example&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example config.js from https://github.com/renovatebot/renovate/blob/main/docs/usage/examples/self-hosting.md&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://self-hosted.gitlab/api/v4/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**gitlab_token**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gitlab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onboardingConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extends&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;config:base&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;repositories&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;username/repo&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;orgname/repo&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;/li&gt;
&lt;li&gt;

&lt;p&gt;&lt;code&gt;.gitlab-ci.yml&lt;/code&gt;. If you need a more complex example, check out &lt;a href="https://gitlab.com/gitlab-org/frontend/renovate-gitlab-bot/-/blob/main/.gitlab-ci.yml"&gt;this example from the GitLab team&lt;/a&gt;. Otherwise, here’s a minimal example; do update accordingly:&lt;br&gt;
&lt;/p&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="s"&gt;renovate/renovate:32.6.12&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debug&lt;/span&gt;

&lt;span class="s"&gt;renovate:on-schedule:&lt;/span&gt;
    &lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;your-gitlab-runner-tag-if-any&lt;/span&gt;
    &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;schedules&lt;/span&gt;
    &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;renovate $RENOVATE_EXTRA_FLAGS&lt;/span&gt;

&lt;span class="na"&gt;renovate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;your-gitlab-runner-tag-if-any&lt;/span&gt;
    &lt;span class="na"&gt;except&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;schedules&lt;/span&gt;
    &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;renovate $RENOVATE_EXTRA_FLAGS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lastly, you’ll need to run Renovate at regular intervals (e.g. every hour) using the GitLab project’s &lt;a href="https://docs.gitlab.com/ee/ci/pipelines/schedules.html"&gt;CI/CD Schedules&lt;/a&gt; feature. Do note that this is different from Renovate’s own &lt;a href="https://docs.renovatebot.com/configuration-options/#schedule"&gt;schedule&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check the &lt;a href="https://gitlab.com/renovate-bot/renovate-runner"&gt;source&lt;/a&gt; for more details. Though, the steps above should be sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  How To Run Renovate Locally
&lt;/h2&gt;

&lt;p&gt;While I was working with a large repository (&amp;gt;6GB for full clone), each Renovate job may take hours to complete. Having the ability to run Renovate locally saves me a bunch of time when it comes to experimentation and debugging.&lt;/p&gt;

&lt;p&gt;First, create a &lt;a href="https://github.com/renovatebot/renovate/blob/main/docs/development/minimal-reproductions.md#how-to-create-a-good-minimal-reproduction"&gt;minimum reproducible example (MRE) repository&lt;/a&gt;. Then, update your &lt;code&gt;config.js&lt;/code&gt; to target or discover the MRE repository. To run Renovate locally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It’s a lot easier to just use &lt;a href="https://www.docker.com/"&gt;Docker&lt;/a&gt;. So, make sure you’ve installed Docker.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;To start, you’ll need to ensure that these environment variables are being exported in your shell:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RENOVATE_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"aa11bb22cc"&lt;/span&gt; &lt;span class="c"&gt;# GitLab Personal Access token (PAT)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GITHUB_COM_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cc33dd44ee"&lt;/span&gt; &lt;span class="c"&gt;# GitHub PAT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Next, update your &lt;code&gt;config.js&lt;/code&gt; accordingly. You’ll need to update your target &lt;code&gt;repositories&lt;/code&gt; accordingly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Finally, you can run Renovate using the following. To grab the latest Renovate version, check out &lt;a href="https://hub.docker.com/r/renovate/renovate/tags"&gt;Docker hub&lt;/a&gt;. Do change the following command accordingly:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"debug"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;GITHUB_COM_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_COM_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"/path/to/local/config.js:/usr/src/app/config.js"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    renovate/renovate:&lt;span class="s2"&gt;"32.6.12"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--token&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RENOVATE_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--dry-run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;With this, testing Renovate configuration in a fast-feedback loop manner is now possible. In case you need more comprehensive logs, try setting &lt;code&gt;LOG_LEVEL=”trace”&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;To perform an actual run, update &lt;code&gt;--dry-run="false"&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  12 Useful Renovate Bot Tips
&lt;/h2&gt;

&lt;h3&gt;
  
  
  General tips
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Not sure how to get the most out of Renovate quickly? Check out this &lt;a href="https://www.augmentedmind.de/2021/07/25/renovate-bot-cheat-sheet/"&gt;Renovate bot cheat sheet&lt;/a&gt; instead of the verboseness of &lt;a href="https://docs.renovatebot.com/"&gt;the official Renovate doc&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;If you are unsure whether Renovate supports a certain functionality, always check out their &lt;a href="https://docs.renovatebot.com/faq/"&gt;FAQ&lt;/a&gt; page first. Chances are it’s already there.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;To disable updates for a specific package or library simply set &lt;code&gt;enabled: false&lt;/code&gt; under the respective &lt;code&gt;[packageRule](https://docs.renovatebot.com/configuration-options/#packagerules)&lt;/code&gt;. Example:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example config.js&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your.domain.com/api/v4/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**gitlab_token**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gitlab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;onboardingConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;extends&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;config:base&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;repositories&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;repository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;john/next-generation-repo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;packageRules&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;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;'Disable MAJOR update types',&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                    &lt;span class="na"&gt;matchUpdateTypes&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;major&lt;/span&gt;&lt;span class="dl"&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;false&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Need to run some custom task or script after upgrading (e.g. a script that posts messages to Slack)? &lt;a href="https://docs.renovatebot.com/configuration-options/#postupgradetasks"&gt;Try &lt;code&gt;postUpgradeTasks&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep project-specific Renovate configs on the bot repository instead of having &lt;code&gt;renovate.json&lt;/code&gt; in every other repository. For this, set &lt;code&gt;[onboarding: false](https://docs.renovatebot.com/self-hosted-configuration/#onboarding)&lt;/code&gt; under &lt;code&gt;module.exports&lt;/code&gt;. This allows for Renovate-related configs to be abstracted away to a single repository only.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Tips on debugging Renovate jobs
&lt;/h3&gt;

&lt;p&gt;When it comes to self-hosted solutions, there’s no running away from debugging your own jobs. There could be scenarios where you’ll need to test connections to your private registry, proxy, etc. &lt;/p&gt;

&lt;p&gt;Here are a couple of helpful tips:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Try running Renovate locally to have a faster feedback loop instead of relying on your CI/CD pipelines. Plus, running locally is practically free!&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;trace&lt;/code&gt; &lt;code&gt;LOG_LEVEL&lt;/code&gt; in case &lt;code&gt;debug&lt;/code&gt; doesn’t give you enough information.&lt;/li&gt;
&lt;li&gt;In case you’re working on some bespoke use cases or facing odd encounters/bugs, try Google searching with the prefix “&lt;em&gt;site:github.com/renovatebot/renovate &lt;/em&gt;”. (&lt;a href="https://www.google.com/search?q=site%3Agithub.com%2Frenovatebot%2Frenovate+wrong+update+version+for+go.mod&amp;amp;ei=6xpJYua5CbiNseMPzbOy2A0&amp;amp;ved=0ahUKEwim2_r3__b2AhW4RmwGHc2ZDNsQ4dUDCA4&amp;amp;uact=5&amp;amp;oq=site%3Agithub.com%2Frenovatebot%2Frenovate+wrong+update+version+for+go.mod&amp;amp;gs_lcp=Cgdnd3Mtd2l6EANKBAhBGAFKBAhGGABQ0CdY2Edg70hoBHAAeAGAAdoBiAGQDZIBBjMyLjAuMZgBAKABAcABAQ&amp;amp;sclient=gws-wiz"&gt;example&lt;/a&gt;). In most cases, you’ll find that there are already others who have filed a similar discussion or issue.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Dealing with a large repository (e.g. GB in size)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;If you attempt to run Renovate on a large repository, you may encounter a &lt;code&gt;SIGTERM&lt;/code&gt; signal (which can be seen in your Renovate job log) due to timeout. To cope with this, increase the &lt;code&gt;[executionTimeout&lt;/code&gt; setting](&lt;a href="https://docs.renovatebot.com/self-hosted-configuration/#executiontimeout"&gt;https://docs.renovatebot.com/self-hosted-configuration/#executiontimeout&lt;/a&gt;) in your &lt;code&gt;config.js&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example config.js&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your.domain.com/api/v4/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;executionTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// minutes&lt;/span&gt;
  &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**gitlab_token**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gitlab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onboardingConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extends&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;config:base&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;repositories&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;john/large-repo&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;doe/huge-repo&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;/li&gt;
&lt;li&gt;&lt;p&gt;Under your GitLab project’s CI/CD General pipelines settings, you may want to increase your job timeout (e.g. 10 hours) as Renovate may take a long time on an uncached run on large repositories.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You may want to set &lt;code&gt;persistRepoData: true&lt;/code&gt; for faster &lt;code&gt;git fetch&lt;/code&gt; between runs.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example config.js&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your.domain.com/api/v4/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**gitlab_token**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gitlab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;persistRepoData&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;onboardingConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;extends&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;config:base&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;repositories&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;john/repo&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;/li&gt;
&lt;li&gt;&lt;p&gt;On some occasions, you may run into &lt;code&gt;ERROR: Disk space error - skipping&lt;/code&gt;. Here, you may want to provision a runner with increased disk size. E.g. if you are using AWS EC2, try to increase the size of the volume.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;Over the past few months, I have been using Renovate bot extensively; on GitHub, and self-hosted GitLab. Today, I am using Renovate on most of my active projects on GitHub to automate dependency updates. So far, Renovate was able to fit 99% of my use cases.&lt;/p&gt;

&lt;p&gt;Keeping our project dependencies up-to-date is often overlooked. Yet, overlooking such work often comes at a great cost. The fact that we can leverage Renovate’s ability to do such grunt work is quite a blessing.&lt;/p&gt;

&lt;p&gt;I hope this article saves you enough time instead of having you groom through pages of GitHub discussions and issues. Thanks for reading!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://jerrynsh.com/12-tips-to-self-host-renovate-bot/"&gt;jerrynsh.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>tooling</category>
      <category>gitlab</category>
      <category>renovate</category>
    </item>
  </channel>
</rss>
