Hey Loura! https://heyloura.com/ en Tue, 28 Apr 2026 13:02:57 -0400 https://heyloura.com/2026/04/28/its-nice-enough-to-each.html Tue, 28 Apr 2026 13:02:57 -0400 http://heyloura.micro.blog/2026/04/28/its-nice-enough-to-each.html <p>It&rsquo;s nice enough to each lunch outside. I&rsquo;m loving this.</p> It's nice enough to each lunch outside. I'm loving this. https://heyloura.com/2026/04/27/sigh-it-looks-like-deno.html Mon, 27 Apr 2026 15:26:15 -0400 http://heyloura.micro.blog/2026/04/27/sigh-it-looks-like-deno.html <p>Sigh, it looks like Deno Deploy Classic is being sunsetted mid-July. Guess I&rsquo;ll have to move, <a href="https://lillihub.com">Lillihub 🐸</a>, my micro.blog client, to the new one or start hosting it myself.</p> Sigh, it looks like Deno Deploy Classic is being sunsetted mid-July. Guess I'll have to move, [Lillihub 🐸](https://lillihub.com), my micro.blog client, to the new one or start hosting it myself. https://heyloura.com/2026/04/21/this-weekend-i-reached-for.html Tue, 21 Apr 2026 13:05:22 -0400 http://heyloura.micro.blog/2026/04/21/this-weekend-i-reached-for.html <p>This weekend I reached for Claude AI to see how it does with an SVG heavy design. It struggled with keeping things in proportion and keeping perspective. I had to nudge it and do hand edits. SVG&rsquo;s are not my forte but together we brought my concept to life. I think its adorable. <a href="https://heyloura.com">Check it out</a>.</p> <img src="https://cdn.uploads.micro.blog/88328/2026/76e4f2bcaf.png" alt="blog with pirate ship design. blog posts go down the sails. at the top there is a pirate and a compass rose for blog navigation."> This weekend I reached for Claude AI to see how it does with an SVG heavy design. It struggled with keeping things in proportion and keeping perspective. I had to nudge it and do hand edits. SVG's are not my forte but together we brought my concept to life. I think its adorable. [Check it out](https://heyloura.com). <img src="https://cdn.uploads.micro.blog/88328/2026/76e4f2bcaf.png" alt="blog with pirate ship design. blog posts go down the sails. at the top there is a pirate and a compass rose for blog navigation."> https://heyloura.com/2026/04/13/sdam-strikes-again-i-was.html Mon, 13 Apr 2026 15:34:59 -0400 http://heyloura.micro.blog/2026/04/13/sdam-strikes-again-i-was.html <p>My poor memory strikes again. I was talking with my sister that I would love, love, love to go see Les Misérables some day on stage when my husband speaks up and tells me he took me to see it already 🤔😆 Woops.</p> My poor memory strikes again. I was talking with my sister that I would love, love, love to go see Les Misérables some day on stage when my husband speaks up and tells me he took me to see it already 🤔😆 Woops. https://heyloura.com/2026/04/08/stylebody-margin-fontfamily-georgia-serif.html Wed, 08 Apr 2026 10:27:04 -0400 http://heyloura.micro.blog/2026/04/08/stylebody-margin-fontfamily-georgia-serif.html <p><a href="https://heyloura.com/2024/04/03/my-son-cracks.html" rel="noopener noreferrer nofollow">Two years</a> later and my other kid needs cookies. At least this time my child checked in the morning well ahead of the lab 😆</p> <p><a href="https://heyloura.com/2024/04/03/my-son-cracks.html" rel="noopener noreferrer nofollow">Two years</a> later and my other kid needs cookies. At least this time my child checked in the morning well ahead of the lab 😆</p> March 8th - April 4th Practice Log https://heyloura.com/2026/04/07/march-th-april-th-practice.html Tue, 07 Apr 2026 09:06:23 -0400 http://heyloura.micro.blog/2026/04/07/march-th-april-th-practice.html <h1 id="tai-chi"><a href="https://heyloura.com/tai-chi">Tai Chi</a></h1> <h2 id="tai-chi-and-kung-fu-practice-log"><em>Tai Chi and Kung Fu Practice Log</em></h2> <h3 id="march-8th---april-4th">March 8th - April 4th</h3> <p><strong>Stats</strong></p> <ul> <li><strong>28 days</strong> logged</li> <li><strong>approx. 42 hours</strong> practiced (class + personal)</li> <li><strong>approx. 4.1 hours</strong> standing meditation</li> <li><strong>20 forms</strong> tai chi &amp; kung fu</li> </ul> <blockquote> <p><em>&ldquo;When the opponent does not move, you will not move; when the opponent slightly moves, you move first.&rdquo;</em></p> <p>— Yáng Bān-Hóu · on Pushing Hands</p> </blockquote> <hr> <h2 id="personal-practice-focus">Personal Practice Focus</h2> <p><strong>01: Opening the Hip Joint</strong> I included daily kua opening exercises, researched the anatomy of the hip, and explored releasing into the hip socket during move transitions.</p> <p><strong>02: Standing Meditation</strong> I worked up from 6-8 minute sessions to 20 min sessions over the month. By April I needed to do less posture corrections.</p> <p><strong>03: Explosive Power</strong> Most moves in Taijiquan have an element of explosive power to them. I drilled silk reeling and fajin expression in my weekly rotation.</p> <p><strong>04: Sensitivity and Structure</strong> Regular weekly pushing hands practice in class and deep form corrections in 64 Guan Ping Yang Form.</p> # [Tai Chi](https://heyloura.com/tai-chi) ## *Tai Chi and Kung Fu Practice Log* ### March 8th - April 4th **Stats** - **28 days** logged - **approx. 42 hours** practiced (class + personal) - **approx. 4.1 hours** standing meditation - **20 forms** tai chi & kung fu > *"When the opponent does not move, you will not move; when the opponent slightly moves, you move first."* > > — Yáng Bān-Hóu · on Pushing Hands --- ## Personal Practice Focus **01: Opening the Hip Joint** I included daily kua opening exercises, researched the anatomy of the hip, and explored releasing into the hip socket during move transitions. **02: Standing Meditation** I worked up from 6-8 minute sessions to 20 min sessions over the month. By April I needed to do less posture corrections. **03: Explosive Power** Most moves in Taijiquan have an element of explosive power to them. I drilled silk reeling and fajin expression in my weekly rotation. **04: Sensitivity and Structure** Regular weekly pushing hands practice in class and deep form corrections in 64 Guan Ping Yang Form. https://heyloura.com/2026/03/27/now-that-i-got-my.html Fri, 27 Mar 2026 09:53:30 -0400 http://heyloura.micro.blog/2026/03/27/now-that-i-got-my.html <p>Now that I got my pkm tool to where I want it, my next <a href="https://heyloura.com/projects" rel="noopener noreferrer nofollow">project</a> is going to be a CMS that lets me build a website like a sticker book.</p> <p>Now that I got my pkm tool to where I want it, my next <a href="https://heyloura.com/projects" rel="noopener noreferrer nofollow">project</a> is going to be a CMS that lets me build a website like a sticker book.</p> https://heyloura.com/2026/03/25/i-feeling-the-digital-mess.html Wed, 25 Mar 2026 10:42:54 -0400 http://heyloura.micro.blog/2026/03/25/i-feeling-the-digital-mess.html <p>I feeling the digital mess of my files across devices pretty strongly today. The urge to toss everything in an archive folder and zip it… is strong. But I'll still know there is a mess of files within it. Don't get me started on digital photos. It's my blog too. It feels messy and disordered right now, not theme-wise (though I kinda want to switch back to my rpg one) but the micro posts and the long posts all together. I've outgrown some categories. I struggle because I both want everything in one place but also organized and consistent. Arg… I guess it's just one of those days.</p> <p>I feeling the digital mess of my files across devices pretty strongly today. The urge to toss everything in an archive folder and zip it… is strong. But I'll still know there is a mess of files within it. Don't get me started on digital photos. It's my blog too. It feels messy and disordered right now, not theme-wise (though I kinda want to switch back to my rpg one) but the micro posts and the long posts all together. I've outgrown some categories. I struggle because I both want everything in one place but also organized and consistent. Arg… I guess it's just one of those days.</p> https://heyloura.com/2026/03/22/im-required-to-use-claude.html Sun, 22 Mar 2026 13:18:10 -0400 http://heyloura.micro.blog/2026/03/22/im-required-to-use-claude.html <img src="https://cdn.uploads.micro.blog/88328/2026/b88df3e31a.png" width="600" height="568" alt="Auto-generated description: A digital journal interface displays an agenda for March 22, 2026, featuring martial arts and fitness activities, personal notes, and a video link."> <p>I&rsquo;m required to use Claude at work so I&rsquo;ve been testing it out. My favorite so far: A PWA quine PKM system. Took what I liked from Logseq (outline - simplified) and single file architecture (Tiddlywiki). I also added in sync to Micro.blog&rsquo;s private notes with chunking. Came together in a day.</p> <img src="https://cdn.uploads.micro.blog/88328/2026/b88df3e31a.png" width="600" height="568" alt="Auto-generated description: A digital journal interface displays an agenda for March 22, 2026, featuring martial arts and fitness activities, personal notes, and a video link."> I'm required to use Claude at work so I've been testing it out. My favorite so far: A PWA quine PKM system. Took what I liked from Logseq (outline - simplified) and single file architecture (Tiddlywiki). I also added in sync to Micro.blog's private notes with chunking. Came together in a day. https://heyloura.com/2026/03/05/ive-had-kids-for-a.html Thu, 05 Mar 2026 14:29:31 -0400 http://heyloura.micro.blog/2026/03/05/ive-had-kids-for-a.html <p>I&rsquo;ve had kid(s) for a decade and a half now, and I&rsquo;m still surprised how early registration opens up for things&hellip; like summer camp. Guess I know what I&rsquo;m researching this evening.</p> I've had kid(s) for a decade and a half now, and I'm still surprised how early registration opens up for things... like summer camp. Guess I know what I'm researching this evening. https://heyloura.com/2026/03/05/read-as-text-using-ai.html Thu, 05 Mar 2026 12:18:37 -0400 http://heyloura.micro.blog/2026/03/05/read-as-text-using-ai.html <map name="usingaiatworkmap"> <area shape="rect" coords="155,95,700,185" href="https://heyloura.com/2026/03/05/read-as-text-using-ai.html" alt="Using AI at work"></map> <div class="handwritten-post-wrap"> <img usemap="#usingaiatworkmap" src="https://cdn.uploads.micro.blog/88328/2026/formatconvert-20260305-104938-8641.webp" class="overflow handwritten-post" alt="Handwritten blog post — transcript below. Doodles: Two grey squiggles adorn the sides and along their bottom, sets of small green plants with pink buds. At the end of the post is a simple drawing of a stick figure shrugging with a thought bubble. Inside the thought bubble is a question mark. Under the stick figure is the word 'shrug'"> </div> <details> <summary>Read as text</summary> <p>Using AI at Work</p> <p>Well, the time finally came. My boss wants me to start using AI in my workflow. In anticipation I've been playing around with Claude Code the last couple of weeks. It's improved since the last time I tried it out.</p> <p>Maybe that's why I'm feeling drawn to this handwriting mood with my blog. Something a bit more me that isn't a quick prompt into existence.</p> <p>Shrug</p> </details> <map name="usingaiatworkmap"> <area shape="rect" coords="155,95,700,185" href="https://heyloura.com/2026/03/05/read-as-text-using-ai.html" alt="Using AI at work"></map> <div class="handwritten-post-wrap"> <img usemap="#usingaiatworkmap" src="https://cdn.uploads.micro.blog/88328/2026/formatconvert-20260305-104938-8641.webp" class="overflow handwritten-post" alt="Handwritten blog post — transcript below. Doodles: Two grey squiggles adorn the sides and along their bottom, sets of small green plants with pink buds. At the end of the post is a simple drawing of a stick figure shrugging with a thought bubble. Inside the thought bubble is a question mark. Under the stick figure is the word 'shrug'"> </div> <details> <summary>Read as text</summary> <p>Using AI at Work</p> <p>Well, the time finally came. My boss wants me to start using AI in my workflow. In anticipation I've been playing around with Claude Code the last couple of weeks. It's improved since the last time I tried it out.</p> <p>Maybe that's why I'm feeling drawn to this handwriting mood with my blog. Something a bit more me that isn't a quick prompt into existence.</p> <p>Shrug</p> </details> https://heyloura.com/2026/03/03/read-as-text-i-got.html Tue, 03 Mar 2026 11:29:02 -0400 http://heyloura.micro.blog/2026/03/03/read-as-text-i-got.html <map name="bloodmooneclipsemap"> <area shape="rect" coords="155,95,700,185" href="https://heyloura.com/2026/03/03/read-as-text-i-got.html" alt="Blood Moon Eclipse"></map> <div class="handwritten-post-wrap"> <img usemap="#bloodmooneclipsemap" src="https://cdn.uploads.micro.blog/88328/2026/formatconvert-20260303-085825-5017.webp" class="overflow handwritten-post" alt="Handwritten blog post — transcript below. Doodles: Three grey cloud shapes and two grey spiral/swirl shapes are scattered around the margins; on the left side is a dark grey cloud with a red/coral half-circle peeking behind it, suggesting a blood moon obscured by clouds."> </div> <details> <summary>Read as text</summary> <p>I got up this morning around 3am because… my brain sucks sometimes. Anyway, it was cloudy then and its still cloudy now. Oh well…</p> <p>♥ Loura</p></details> <map name="bloodmooneclipsemap"> <area shape="rect" coords="155,95,700,185" href="https://heyloura.com/2026/03/03/read-as-text-i-got.html" alt="Blood Moon Eclipse"></map> <div class="handwritten-post-wrap"> <img usemap="#bloodmooneclipsemap" src="https://cdn.uploads.micro.blog/88328/2026/formatconvert-20260303-085825-5017.webp" class="overflow handwritten-post" alt="Handwritten blog post — transcript below. Doodles: Three grey cloud shapes and two grey spiral/swirl shapes are scattered around the margins; on the left side is a dark grey cloud with a red/coral half-circle peeking behind it, suggesting a blood moon obscured by clouds."> </div> <details> <summary>Read as text</summary> <p>I got up this morning around 3am because… my brain sucks sometimes. Anyway, it was cloudy then and its still cloudy now. Oh well…</p> <p>♥ Loura</p></details> https://heyloura.com/2026/03/02/read-as-text-my-kid.html Mon, 02 Mar 2026 22:15:00 -0400 http://heyloura.micro.blog/2026/03/02/read-as-text-my-kid.html <map name="thewaitinggamemap"> <area shape="rect" coords="155,95,700,185" href="https://heyloura.com/2026/03/02/read-as-text-my-kid.html" alt="The Waiting Game"></map> <div class="handwritten-post-wrap"> <img usemap="#thewaitinggamemap" src="https://cdn.uploads.micro.blog/88328/2026/formatconvert-20260303-085746-6314.webp" class="overflow handwritten-post" alt="Handwritten blog post — transcript below. A decorative border of two curving rose vines with green leaves runs down the left and right sides, each topped with a pink/red spiral rosebud; a small red broken heart appears inline before the second paragraph."> </div> <details> <summary>Read as text</summary> <p>My kid fell in love with the local agricultural school during a summer camp session. This year our state switched all technical schools to be lottery based. We filled out the application and today was the day. We waited…</p> <p>No luck. I have a very sad kid.</p></details> <map name="thewaitinggamemap"> <area shape="rect" coords="155,95,700,185" href="https://heyloura.com/2026/03/02/read-as-text-my-kid.html" alt="The Waiting Game"></map> <div class="handwritten-post-wrap"> <img usemap="#thewaitinggamemap" src="https://cdn.uploads.micro.blog/88328/2026/formatconvert-20260303-085746-6314.webp" class="overflow handwritten-post" alt="Handwritten blog post — transcript below. A decorative border of two curving rose vines with green leaves runs down the left and right sides, each topped with a pink/red spiral rosebud; a small red broken heart appears inline before the second paragraph."> </div> <details> <summary>Read as text</summary> <p>My kid fell in love with the local agricultural school during a summer camp session. This year our state switched all technical schools to be lottery based. We filled out the application and today was the day. We waited…</p> <p>No luck. I have a very sad kid.</p></details> Handwriting a blog https://heyloura.com/2026/03/01/handwriting-a-blog.html Sun, 01 Mar 2026 22:15:00 -0400 http://heyloura.micro.blog/2026/03/01/handwriting-a-blog.html <map name="map1"> <area shape="rect" coords="200,30,900,175" href="https://heyloura.com/2026/03/01/handwriting-a-blog.html" alt="Handwriting a blog – March 1st 2026"> <area shape="rect" coords="175,190,880,880" href="https://web.archive.org/web/20250227184810/https://www.handwritten.blog" alt="Link reference [1]: handwritten.blog on the Internet Archive"> <area shape="rect" coords="940,180,990,880" href="https://web.archive.org/web/20250227184810/https://www.handwritten.blog" alt="Link reference [1]: handwritten.blog on the Internet Archive"> </map> <div class="handwritten-post-wrap"> <img usemap="#map1" src="https://cdn.uploads.micro.blog/88328/2026/4ec2861eb2.webp" class="overflow handwritten-post" alt="Handwritten blog post — transcript below. Decorative doodles: Left margin: a vertical trail of blue teardrop/raindrop shapes, and a blue swirl/spiral shape Inline after mind wander.: a small drawing of a swimmer with a red cap and goggles, with blue splashes Bottom of page: a row of colorful triangular bunting/pennant flags in red, yellow, and blue"> </div> <div class="handwritten-post-wrap"> <img src="https://cdn.uploads.micro.blog/88328/2026/1a95339342.webp" class="overflow handwritten-post" alt="Handwritten blog post — transcript below. Decorative doodles: Upper left: a blue flower with petals splayed in two directions, like a snowflake or asterisk shape. Between paragraphs: a horizontal divider made of small blue leaf/teardrop shapes in a row. Left of second paragraph: a stick figure mermaid with a yellow hair and a curvy green tail"> </div> <details> <summary>Read as text</summary> <p>One of the nice things about going to my kids swim meet is that I have time to let my mind wander.</p> <p>A few years ago, I came across a website called "Handwritten. blog." Sadly, it is no longer hosted but you can still see some of it on the internet archive [1].</p> <p>Maybe it was because I was reading on my e-ink tablet. Or perhaps I'm just tired of my current blog design again. But why not give it a shot?</p> <p>One thing is obvious, editing posts will be a nightmare. Usually I take my time and will rewrite a blog post a few times. Not gonna happen if I'm writing these out long hand. So yeah… a little unpolished it is.</p> <p>♥ Loura</p> <p><strong>Links</strong><br> [1] <a href="https://web.archive.org/web/20250227184810/https://www.handwritten.blog">Internet Archive</a> </p> </details> <map name="map1"> <area shape="rect" coords="200,30,900,175" href="https://heyloura.com/2026/03/01/handwriting-a-blog.html" alt="Handwriting a blog – March 1st 2026"> <area shape="rect" coords="175,190,880,880" href="https://web.archive.org/web/20250227184810/https://www.handwritten.blog" alt="Link reference [1]: handwritten.blog on the Internet Archive"> <area shape="rect" coords="940,180,990,880" href="https://web.archive.org/web/20250227184810/https://www.handwritten.blog" alt="Link reference [1]: handwritten.blog on the Internet Archive"> </map> <div class="handwritten-post-wrap"> <img usemap="#map1" src="https://cdn.uploads.micro.blog/88328/2026/4ec2861eb2.webp" class="overflow handwritten-post" alt="Handwritten blog post — transcript below. Decorative doodles: Left margin: a vertical trail of blue teardrop/raindrop shapes, and a blue swirl/spiral shape Inline after mind wander.: a small drawing of a swimmer with a red cap and goggles, with blue splashes Bottom of page: a row of colorful triangular bunting/pennant flags in red, yellow, and blue"> </div> <div class="handwritten-post-wrap"> <img src="https://cdn.uploads.micro.blog/88328/2026/1a95339342.webp" class="overflow handwritten-post" alt="Handwritten blog post — transcript below. Decorative doodles: Upper left: a blue flower with petals splayed in two directions, like a snowflake or asterisk shape. Between paragraphs: a horizontal divider made of small blue leaf/teardrop shapes in a row. Left of second paragraph: a stick figure mermaid with a yellow hair and a curvy green tail"> </div> <details> <summary>Read as text</summary> <p>One of the nice things about going to my kids swim meet is that I have time to let my mind wander.</p> <p>A few years ago, I came across a website called "Handwritten. blog." Sadly, it is no longer hosted but you can still see some of it on the internet archive [1].</p> <p>Maybe it was because I was reading on my e-ink tablet. Or perhaps I'm just tired of my current blog design again. But why not give it a shot?</p> <p>One thing is obvious, editing posts will be a nightmare. Usually I take my time and will rewrite a blog post a few times. Not gonna happen if I'm writing these out long hand. So yeah… a little unpolished it is.</p> <p>♥ Loura</p> <p><strong>Links</strong><br> [1] <a href="https://web.archive.org/web/20250227184810/https://www.handwritten.blog">Internet Archive</a> </p> </details> https://heyloura.com/2026/02/23/our-town-reported-in-of.html Mon, 23 Feb 2026 19:31:12 -0400 http://heyloura.micro.blog/2026/02/23/our-town-reported-in-of.html <p>Our town reported 25in of snow for today&rsquo;s blizzard. Though I feel like we got more. Not the best day for a shear pin to break on the snow blower. So we got to hand shovel the way too long driveway.</p> <img src="https://cdn.uploads.micro.blog/88328/2026/5f81ff7be9.jpg" width="450" height="600" alt="Women in snow gear standing in front of large snow pile and bamboo."> Our town reported 25in of snow for today's blizzard. Though I feel like we got more. Not the best day for a shear pin to break on the snow blower. So we got to hand shovel the way too long driveway. <img src="https://cdn.uploads.micro.blog/88328/2026/5f81ff7be9.jpg" width="450" height="600" alt="Women in snow gear standing in front of large snow pile and bamboo."> https://heyloura.com/2026/01/31/i-purchased-a-variety-of.html Sat, 31 Jan 2026 17:28:44 -0400 http://heyloura.micro.blog/2026/01/31/i-purchased-a-variety-of.html <p>I purchased a variety of green teas to try for the new year. As a way to take quiet moments and enjoy some health benefits. Today&rsquo;s selection: premium dragon well long jing. It has a nice smell and its quite mellow.</p> <img src="https://cdn.uploads.micro.blog/88328/2026/jpeg-20260131-162731-3582694327120505276.jpg" width="600" height="600" alt=""> I purchased a variety of green teas to try for the new year. As a way to take quiet moments and enjoy some health benefits. Today's selection: premium dragon well long jing. It has a nice smell and its quite mellow. <img src="https://cdn.uploads.micro.blog/88328/2026/jpeg-20260131-162731-3582694327120505276.jpg" width="600" height="600" alt=""> https://heyloura.com/2026/01/30/i-downloaded-the-hobonichi-app.html Fri, 30 Jan 2026 19:12:26 -0400 http://heyloura.micro.blog/2026/01/30/i-downloaded-the-hobonichi-app.html <p>I downloaded the hobonichi app to see if I liked it more than day one. It is cuter which may help me stick, but I like the encrypted option of day one and with day one I can use the android share sheet and get thoughts like these into Micro.blog. I&rsquo;m going to give each a month and then decide.</p> I downloaded the hobonichi app to see if I liked it more than day one. It is cuter which may help me stick, but I like the encrypted option of day one and with day one I can use the android share sheet and get thoughts like these into Micro.blog. I'm going to give each a month and then decide. App Defaults 2026 https://heyloura.com/2026/01/30/app-defaults.html Fri, 30 Jan 2026 09:46:42 -0400 http://heyloura.micro.blog/2026/01/30/app-defaults.html <p>Quite a few changes from my last update in <a href="https://heyloura.com/2023/12/07/my-app-defaults.html">2023</a>:</p> <p>📧 Email Client (Web): Fastmail</p> <p>📧 Email Client (Android): e/os default mail app (K-9 Mail fork)</p> <p>🕸️ Website: Micro.blog & Namecheap</p> <p>📝✅ Notes + Todos: Index cards, Micro.blog private notes (both notes and todos), and Logseq (for work notes)</p> <p>📔 Journal: Hobonichi 5 year, trying out Day one</p> <p>📸 Photo Management: Ente</p> <p>🗓️ Calendar (Web): Fastmail</p> <p>🗓️ Calendar (Android): e/os default calendar app</p> <p>🎁 Cloud file storage (unencrypted): Fastmail</p> <p>🎁 Cloud file storage (encrypted): 1password</p> <p>👽 Contacts: e/os default Android app</p> <p>🌐 Browser: Brave (hoping for Kagi browser soon)</p> <p>🔎 Search Engine: Kagi</p> <p>💬 Chat: e/os default + Signal</p> <p>🔖 Bookmarks: Micro.blog</p> <p>💵 Budgeting: self-hosted dumb budget app</p> <p>💵 Personal Finance: Micro.blog private notes (self aggregated data)</p> <p>📰 News: Allsides</p> <p>🔐 Password Management: 1password</p> <p>🎨 Photo Editing: Paint.Net</p> <p>👩‍💻 Code Editor: Visual Studio Code or Visual Studio</p> <p>📚 Books: Self-hosted - audiobookshelf + physical copies</p> Quite a few changes from my last update in [2023](https://heyloura.com/2023/12/07/my-app-defaults.html): <p>📧 Email Client (Web): Fastmail</p> <p>📧 Email Client (Android): e/os default mail app (K-9 Mail fork)</p> <p>🕸️ Website: Micro.blog & Namecheap</p> <p>📝✅ Notes + Todos: Index cards, Micro.blog private notes (both notes and todos), and Logseq (for work notes)</p> <p>📔 Journal: Hobonichi 5 year, trying out Day one</p> <p>📸 Photo Management: Ente</p> <p>🗓️ Calendar (Web): Fastmail</p> <p>🗓️ Calendar (Android): e/os default calendar app</p> <p>🎁 Cloud file storage (unencrypted): Fastmail</p> <p>🎁 Cloud file storage (encrypted): 1password</p> <p>👽 Contacts: e/os default Android app</p> <p>🌐 Browser: Brave (hoping for Kagi browser soon)</p> <p>🔎 Search Engine: Kagi</p> <p>💬 Chat: e/os default + Signal</p> <p>🔖 Bookmarks: Micro.blog</p> <p>💵 Budgeting: self-hosted dumb budget app</p> <p>💵 Personal Finance: Micro.blog private notes (self aggregated data)</p> <p>📰 News: Allsides</p> <p>🔐 Password Management: 1password</p> <p>🎨 Photo Editing: Paint.Net</p> <p>👩‍💻 Code Editor: Visual Studio Code or Visual Studio</p> <p>📚 Books: Self-hosted - audiobookshelf + physical copies</p> https://heyloura.com/2026/01/28/i-had-a-good-time.html Wed, 28 Jan 2026 22:36:49 -0400 http://heyloura.micro.blog/2026/01/28/i-had-a-good-time.html <p>I had a good time attending a virtual Homebrew Website Club meeting this evening while I worked on my blog redesign. I was inspired by a codepen I came across for a journal with flipping pages done with CSS. The design only shows for desktop and its still a work in progress, but I&rsquo;m having a blast.</p> I had a good time attending a virtual Homebrew Website Club meeting this evening while I worked on my blog redesign. I was inspired by a codepen I came across for a journal with flipping pages done with CSS. The design only shows for desktop and its still a work in progress, but I'm having a blast. https://heyloura.com/2026/01/27/i-didnt-realize-day-one.html Tue, 27 Jan 2026 21:37:52 -0400 http://heyloura.micro.blog/2026/01/27/i-didnt-realize-day-one.html <p>I didn&rsquo;t realize day one now had a web app and an android app. I&rsquo;m going to have to check it out.</p> I didn't realize day one now had a web app and an android app. I'm going to have to check it out. https://heyloura.com/2026/01/23/planting-bamboo-along-a-driveway.html Fri, 23 Jan 2026 13:35:24 -0400 http://heyloura.micro.blog/2026/01/23/planting-bamboo-along-a-driveway.html <p>Planting bamboo along a driveway is fun! Ignore that it&rsquo;s impassable mess when covered in snow. Every winter storm we need to keep shaking the snow off to keep it from freezing to the ground. Maybe this is why the previous owner left 🤣 (they planted it).</p> <img src="https://cdn.uploads.micro.blog/88328/2026/img-20260119-091519.jpg" width="600" height="450" alt="Snow-covered bamboo blocks a driveway with a backdrop of winter forest scene."> Planting bamboo along a driveway is fun! Ignore that it's impassable mess when covered in snow. Every winter storm we need to keep shaking the snow off to keep it from freezing to the ground. Maybe this is why the previous owner left 🤣 (they planted it). <img src="https://cdn.uploads.micro.blog/88328/2026/img-20260119-091519.jpg" width="600" height="450" alt="Snow-covered bamboo blocks a driveway with a backdrop of winter forest scene."> https://heyloura.com/2026/01/11/i-forgot-how-loud-an.html Sun, 11 Jan 2026 13:19:06 -0400 http://heyloura.micro.blog/2026/01/11/i-forgot-how-loud-an.html <p>I forgot how loud an indoor water park can get. The kids are having fun though 😄</p> I forgot how loud an indoor water park can get. The kids are having fun though 😄 https://heyloura.com/2026/01/01/weve-been-sick-with-the.html Thu, 01 Jan 2026 13:40:31 -0400 http://heyloura.micro.blog/2026/01/01/weve-been-sick-with-the.html <p>We&rsquo;ve been sick with the flu the last two weeks so I&rsquo;ve done zero prep for the new year. But waking up to a fresh dusting of snow was nice.</p> <img src="https://cdn.uploads.micro.blog/88328/2026/387f21556c.jpg" width="450" height="600" alt="A snow-covered landscape with bare trees is visible through a rectangular window."> We've been sick with the flu the last two weeks so I've done zero prep for the new year. But waking up to a fresh dusting of snow was nice. <img src="https://cdn.uploads.micro.blog/88328/2026/387f21556c.jpg" width="450" height="600" alt="A snow-covered landscape with bare trees is visible through a rectangular window."> https://heyloura.com/2025/12/26/i-had-a-lovely-holiday.html Fri, 26 Dec 2025 13:33:04 -0400 http://heyloura.micro.blog/2025/12/26/i-had-a-lovely-holiday.html <p>I had a lovely holiday and I&rsquo;m on vacation until the new year. I&rsquo;m going to spend time catching up on my five year journal and planning out the next five years. There are college plans for one kid, figuring out high school plans for another and finishing up my &ldquo;homeschooling the kids&rdquo; journey.</p> I had a lovely holiday and I'm on vacation until the new year. I'm going to spend time catching up on my five year journal and planning out the next five years. There are college plans for one kid, figuring out high school plans for another and finishing up my "homeschooling the kids" journey. Supabase, Deno and Server Side Rendering https://heyloura.com/2025/12/22/supabase-deno-and-server-side.html Mon, 22 Dec 2025 11:16:37 -0400 http://heyloura.micro.blog/2025/12/22/supabase-deno-and-server-side.html <p>A project I&rsquo;m currently working on is using <a href="https://supabase.com/">Supabase</a> for its database and authentication layer. And so far so good. That is until I wanted to move all the calls to the Supabase API server side for server side rendering (SSR). Now, they do have lots of documentation and tutorials out there and a bunch of third party tutorials all over the web. But nothing quite fit what I was trying to do. I&rsquo;m using <a href="https://deno.com/">Deno</a>, for server side JavaScript, and I figured with Supabase&rsquo;s SSR package it would be a pretty easy to implement. You know what they say about assumptions 😆</p> <p>Since I&rsquo;m not using any additional framework, their tutorial documentation wasn&rsquo;t all that helpful. But it did give me enough of a sense of what I needed to do. Which boils down to setting cookies, reading cookies, and calling the Supabase client library. Then digging around Github, Github Issues, and Stack Overflow I eventually pieced it together.</p> <p>So, to maybe save someone else (or an AI scraper) some time in the future. Here is I tied up Supabase SSR with Deno and no additional framework:</p> <h1 id="putting-it-all-together">Putting it all together</h1> <h2 id="logging-in-a-user">Logging in a user</h2> <ol> <li> <p>I&rsquo;ve saved some key info in enviromental variables, namely <code>Deno.env.get(&quot;SUPABASE_URL&quot;)</code> and <code>Deno.env.get(&quot;SUPABASE_ANON_KEY&quot;)</code></p> </li> <li> <p>I&rsquo;m importing the supabase client with the following statement at the top of my main.js file: <code>import { createClient } from &quot;jsr:@supabase/supabase-js@2&quot;</code></p> </li> <li> <p>I have a login page with a simple HTML form that does a <code>POST</code> request to an endpoint and then calls the <code>createClient</code> to determine if the credentials are correct and we have a valid user logging in. This method also creates three cookies with the request. The first two are domain bound, secure, and HTTP Only cookies with expirations.</p> <ol> <li> <p><code>auth-token</code> this is returned from the successful createClient call with an accompyining expiration which is 1 hour after issue.</p> </li> <li> <p><code>refresh-token</code> is needed to refresh the session without having the user log back into the application with credentials. If you need it to stick around longer, adjust the <code>MaxAge</code></p> </li> <li> <p><code>auth-issued-at</code> is needed for the client to calculate when to prompt the user if they want to continue with their session (for my use case, 5 minutes before the auth-token expires). A lot of modern apps skip this and would just grab the refresh-token for a more seamless experience, but I&rsquo;m going to need it. Also this cookie is not HTTP only, since I need to access the value on the client.</p> </li> </ol> </li> </ol> <p>Here is the code so far (simplified)</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">createClient</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;jsr:@supabase/supabase-js@2&#34;</span>; </span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">_domain</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;my-awesome-app.com&#39;</span>; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#a6e22e">Deno</span>.<span style="color:#a6e22e">serve</span>(<span style="color:#66d9ef">async</span> (<span style="color:#a6e22e">req</span>) =&gt; { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">URLPattern</span>({ <span style="color:#a6e22e">pathname</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;/sign-in&#34;</span> }).<span style="color:#a6e22e">exec</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">url</span>)) { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">method</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#34;POST&#34;</span>) { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">formData</span>(); </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">email</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">value</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;email&#39;</span>); </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">password</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">value</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;password&#39;</span>); </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">supabase</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">createClient</span>(<span style="color:#a6e22e">Deno</span>.<span style="color:#a6e22e">env</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#34;SUPABASE_URL&#34;</span>), <span style="color:#a6e22e">Deno</span>.<span style="color:#a6e22e">env</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#34;SUPABASE_ANON_KEY&#34;</span>)); </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">data</span>, <span style="color:#a6e22e">error</span> } <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">supabase</span>.<span style="color:#a6e22e">auth</span>.<span style="color:#a6e22e">signInWithPassword</span>({ <span style="color:#a6e22e">email</span>, <span style="color:#a6e22e">password</span> }); </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">error</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">session</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">user</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">null</span>) { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#e6db74">&#34;We could not log you into our application&#34;</span>, { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">403</span>, </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> { </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;content-type&#34;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;text/plain&#34;</span> </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> }); </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">response</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#e6db74">`&lt;h1&gt;Yay! Go to your &lt;a href=&#34;/dashboard&#34;&gt;dashboard&lt;/a&gt;&lt;/h1&gt;`</span>, { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">200</span>, </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> { </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;content-type&#34;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;text/html&#34;</span>, </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> }); </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">headers</span>.<span style="color:#a6e22e">append</span>(<span style="color:#e6db74">&#34;set-cookie&#34;</span>, <span style="color:#e6db74">`auth-token=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">session</span>.<span style="color:#a6e22e">access_token</span><span style="color:#e6db74">}</span><span style="color:#e6db74">;domain:</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">_domain</span><span style="color:#e6db74">}</span><span style="color:#e6db74">;SameSite=Lax;Path=/;Secure;HttpOnly;MaxAge=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">session</span>.<span style="color:#a6e22e">expires_in</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>); </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">headers</span>.<span style="color:#a6e22e">append</span>(<span style="color:#e6db74">&#34;set-cookie&#34;</span>, <span style="color:#e6db74">`refresh-token=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">session</span>.<span style="color:#a6e22e">refresh_token</span><span style="color:#e6db74">}</span><span style="color:#e6db74">;domain:</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">_domain</span><span style="color:#e6db74">}</span><span style="color:#e6db74">;SameSite=Lax;Path=/;Secure;HttpOnly;MaxAge=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">session</span>.<span style="color:#a6e22e">expires_in</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>); </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">headers</span>.<span style="color:#a6e22e">append</span>(<span style="color:#e6db74">&#34;set-cookie&#34;</span>, <span style="color:#e6db74">`auth-issued-at=</span><span style="color:#e6db74">${</span>Date.<span style="color:#a6e22e">now</span>()<span style="color:#e6db74">}</span><span style="color:#e6db74">;domain:</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">_domain</span><span style="color:#e6db74">}</span><span style="color:#e6db74">;SameSite=Lax;Path=/;Secure;MaxAge=</span><span style="color:#e6db74">${</span><span style="color:#ae81ff">60</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">60</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">24</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">180</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>); </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">response</span>; </span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">method</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#34;GET&#34;</span>) { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">signInTemplate</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">TextDecoder</span>().<span style="color:#a6e22e">decode</span>(<span style="color:#66d9ef">await</span> <span style="color:#a6e22e">Deno</span>.<span style="color:#a6e22e">readFile</span>(<span style="color:#e6db74">&#34;layouts/sign-in.html&#34;</span>)); </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#a6e22e">signInTemplate</span>, { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">200</span>, </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> { </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;content-type&#34;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;text/html&#34;</span> </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> }); </span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">else</span> { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">method</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> is not supported`</span>, { <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">405</span> }); </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span>}); </span></span></code></pre></div><h2 id="reading-the-saved-cookies-on-the-server">Reading the saved cookies on the server</h2> <p>Cookies are passed along with each request from the browser. I use a simple function that takes in the request, finds the cookie I ask for and then passes back the value.</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#75715e">// req is the request object from Deno.serve </span></span></span><span style="display:flex;"><span><span style="color:#75715e">// name is the name of the cookie to find </span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getCookieValue</span>(<span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">name</span>) { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">cookies</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">headers</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#34;cookie&#34;</span>) <span style="color:#f92672">?</span> <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">headers</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#34;cookie&#34;</span>).<span style="color:#a6e22e">split</span>(<span style="color:#e6db74">&#39;; &#39;</span>) <span style="color:#f92672">:</span> []; </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">cookieValue</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">cookies</span>.<span style="color:#a6e22e">filter</span>(<span style="color:#a6e22e">cookie</span> =&gt; <span style="color:#a6e22e">cookie</span>.<span style="color:#a6e22e">includes</span>(<span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">name</span><span style="color:#e6db74">}</span><span style="color:#e6db74">=`</span>)); </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">cookieValue</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">cookieValue</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">?</span> <span style="color:#a6e22e">cookieValue</span>[<span style="color:#ae81ff">0</span>].<span style="color:#a6e22e">split</span>(<span style="color:#e6db74">&#39;=&#39;</span>)[<span style="color:#ae81ff">1</span>] <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;&#39;</span>; </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">cookieValue</span>; </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><h2 id="protecting-endpoints">Protecting endpoints</h2> <ol> <li>Create a user-aware Supabase client. The one we&rsquo;ve been using up until now has just been using the anonymous keys which aren&rsquo;t tied to a particular user. However, the database has been set up with a bunch of Row-Level Security (RLS) Policies that do care who the user is. From this <a href="">Github issue</a> I put together a function that takes in the user&rsquo;s auth token from supabase and then creates a user aware client. I don&rsquo;t have <code>persistSession</code> or <code>autoResfreshSession</code> set, since I will be handling those myself.</li> <li>Every protected route calls my verify user function that takes a users auth token, verifys the token with a call to the unauthenticated Supabase client and if it looks good, creates and sets the user-aware Supabase client that the rest of the code for that endpoint will use.</li> <li>If the auth token is expired or bad, send that information back with the request so my webpage can redirect the user to a login page. (NOTE, some may just grab the refresh token now and use it, depends on business requirements)</li> </ol> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">let</span> <span style="color:#a6e22e">_supabase</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#75715e">//https://github.com/supabase/supabase/issues/8490#issuecomment-1219766620 </span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">createServerDbClient</span>(<span style="color:#a6e22e">accessToken</span>) { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">createClient</span>(<span style="color:#a6e22e">Deno</span>.<span style="color:#a6e22e">env</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#34;SUPABASE_URL&#34;</span>), <span style="color:#a6e22e">Deno</span>.<span style="color:#a6e22e">env</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#34;SUPABASE_ANON_KEY&#34;</span>), { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">db</span><span style="color:#f92672">:</span> { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">schema</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;public&#39;</span>, </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">auth</span><span style="color:#f92672">:</span> { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">persistSession</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>, </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">autoRefreshToken</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>, </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">global</span><span style="color:#f92672">:</span> { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">accessToken</span> <span style="color:#f92672">?</span> { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Authorization</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`Bearer </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">accessToken</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>, </span></span><span style="display:flex;"><span> } <span style="color:#f92672">:</span> <span style="color:#66d9ef">null</span>, </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> }); </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#75715e">// called before any protected endpoint and result checked. </span></span></span><span style="display:flex;"><span><span style="color:#75715e">// req is the request object from Deno.serve </span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">VerifyUser</span>(<span style="color:#a6e22e">req</span>) { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">token</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">getCookieValue</span>(<span style="color:#a6e22e">req</span>, <span style="color:#e6db74">&#39;auth-token&#39;</span>); </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span>(<span style="color:#f92672">!</span><span style="color:#a6e22e">token</span>) { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> {<span style="color:#a6e22e">verified</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">error</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`No token found`</span>}; </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> <span style="color:#75715e">// unauthenticated supabase client </span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">supabaseClient</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">createClient</span>(<span style="color:#a6e22e">Deno</span>.<span style="color:#a6e22e">env</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#34;SUPABASE_URL&#34;</span>), <span style="color:#a6e22e">Deno</span>.<span style="color:#a6e22e">env</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#34;SUPABASE_ANON_KEY&#34;</span>)); </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">try</span> { </span></span><span style="display:flex;"><span> <span style="color:#75715e">// check what supabase says, </span></span></span><span style="display:flex;"><span> <span style="color:#75715e">// getClaims throws exception with expired token </span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">data</span>, <span style="color:#a6e22e">error</span> } <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">supabaseClient</span>.<span style="color:#a6e22e">auth</span>.<span style="color:#a6e22e">getClaims</span>(<span style="color:#a6e22e">token</span>); </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">error</span>) { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> {<span style="color:#a6e22e">verified</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">error</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">error</span>}; </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">catch</span> { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> {<span style="color:#a6e22e">verified</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">error</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Supabase client exception thrown&#39;</span>}; </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> <span style="color:#75715e">// looks good, let the user through and set the supabase client for use </span></span></span><span style="display:flex;"><span> <span style="color:#a6e22e">_supabase</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">createServerDbClient</span>(<span style="color:#a6e22e">token</span>); </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> {<span style="color:#a6e22e">verified</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>, <span style="color:#a6e22e">error</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">null</span>}; </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><h2 id="this-will-work-for-an-hour">This will work&hellip; for an hour</h2> <p>Calls to my protected endpoint will work for an hour before that auth token expires. So, I needed to check on the webpage when the token is going to expire so I can alert the user. That&rsquo;s why I set the <code>auth-issued-at</code> cookie to be readable by JavaScript on the webpage. The basic plan is to figure out the current time, the time the auth code was issued and how long it has left. For my purposes, I do the following:</p> <ol> <li> <p>Longer that 5 minutes left? Create a call to <code>setTimeout</code> that will alert the user when they have five minutes left.</p> </li> <li> <p>Less than 5 minutes? Alert the user.</p> </li> <li> <p>Expired? Send the user to the login page.</p> </li> </ol> <p>The alert is a modal that lets the user choose to continue working (i.e. refresh the session) or to log them out. Assuming they want to continue then I call the endpoint to refresh the session. But there is the case to consider that the alert was up and received no input for longer than the time to expire. In that case I show another alert on any interaction that the session expired and kick them back to the login page.</p> <h2 id="using-the-refresh-token">Using the refresh token</h2> <p>I protect the refresh endpoint just like I do other protected endpoints, I make sure the user has a valid and unexpired token before letting a call go through. This is a business requirement I have, others for a more seamless workflow may allow a refresh token to be used after a session has already expired.</p> <p>The trickest part was figuring out what endpoint to call. There doesn&rsquo;t seem to be any good documentation around it so I needed to dig around a bunch to find what other libraries were doing in their code. But once the sleuthing was done it was pretty straight forward.</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#75715e">// verify the user can access restricted content </span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">verify</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">VerifyUser</span>(<span style="color:#a6e22e">req</span>); </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span>(<span style="color:#f92672">!</span><span style="color:#a6e22e">verify</span>.<span style="color:#a6e22e">verified</span>) { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#e6db74">`Please log back in: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">verify</span>.<span style="color:#a6e22e">error</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>, { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">404</span>, </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> { </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;content-type&#34;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;text/html&#34;</span> </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> }); </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#75715e">// user requested to refresh the session </span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">URLPattern</span>({ <span style="color:#a6e22e">pathname</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;/refresh_session&#34;</span> }).<span style="color:#a6e22e">exec</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">url</span>)) { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">method</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#34;POST&#34;</span>) { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">refreshToken</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">getCookieValue</span>(<span style="color:#a6e22e">req</span>, <span style="color:#e6db74">&#39;refresh-token&#39;</span>); </span></span><span style="display:flex;"><span> <span style="color:#75715e">// send the token to supabase </span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">fetching</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fetch</span>(<span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">Deno</span>.<span style="color:#a6e22e">env</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#34;SUPABASE_URL&#34;</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">/auth/v1/token?grant_type=refresh_token`</span>, { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">method</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;POST&#39;</span>, </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> { </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;Content-Type&#39;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;application/json&#39;</span>, </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;apikey&#39;</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">Deno</span>.<span style="color:#a6e22e">env</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#34;SUPABASE_ANON_KEY&#34;</span>) </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">body</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">stringify</span>({ </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">refresh_token</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">refreshToken</span>, </span></span><span style="display:flex;"><span> }) </span></span><span style="display:flex;"><span> }); </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">data</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fetching</span>.<span style="color:#a6e22e">json</span>(); </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">// if we don&#39;t get a new token, then the refresh failed </span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span>(<span style="color:#f92672">!</span><span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">access_token</span>) { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#e6db74">&#39;Refresh failed&#39;</span>, { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">403</span>, </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> { </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;content-type&#34;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;text/html&#34;</span>, </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> }); </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">// everything worked, refresh the cookies </span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">response</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#e6db74">&#39;Processed&#39;</span>, { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">200</span>, </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> { </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;content-type&#34;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;text/html&#34;</span>, </span></span><span style="display:flex;"><span> }, </span></span><span style="display:flex;"><span> }); </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">headers</span>.<span style="color:#a6e22e">append</span>(<span style="color:#e6db74">&#34;set-cookie&#34;</span>,<span style="color:#e6db74">`auth-token=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">access_token</span><span style="color:#e6db74">}</span><span style="color:#e6db74">;domain:</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">_domain</span><span style="color:#e6db74">}</span><span style="color:#e6db74">;SameSite=Lax;Path=/;Secure;HttpOnly;MaxAge=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">expires_in</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>); </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">headers</span>.<span style="color:#a6e22e">append</span>(<span style="color:#e6db74">&#34;set-cookie&#34;</span>,<span style="color:#e6db74">`refresh-token=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">refresh_token</span><span style="color:#e6db74">}</span><span style="color:#e6db74">;domain:</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">_domain</span><span style="color:#e6db74">}</span><span style="color:#e6db74">;SameSite=Lax;Path=/;Secure;HttpOnly;MaxAge=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">expires_in</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>); </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">headers</span>.<span style="color:#a6e22e">append</span>(<span style="color:#e6db74">&#34;set-cookie&#34;</span>,<span style="color:#e6db74">`auth-issued-at=</span><span style="color:#e6db74">${</span>Date.<span style="color:#a6e22e">now</span>()<span style="color:#e6db74">}</span><span style="color:#e6db74">;domain:</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">_domain</span><span style="color:#e6db74">}</span><span style="color:#e6db74">;SameSite=Lax;Path=/;Secure;MaxAge=</span><span style="color:#e6db74">${</span><span style="color:#ae81ff">60</span><span style="color:#f92672">*</span><span style="color:#ae81ff">60</span><span style="color:#f92672">*</span><span style="color:#ae81ff">24</span><span style="color:#f92672">*</span><span style="color:#ae81ff">180</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>); </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">response</span>; </span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">else</span> { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">method</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> is not supported`</span>, { <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">405</span> }); </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> } </span></span></code></pre></div><h2 id="and-done">And done!</h2> <p>Now I have a blueprint on how to log users in, save the needed cookies, and how to use the refresh token to get another auth token when needed.</p> A project I'm currently working on is using [Supabase](https://supabase.com/) for its database and authentication layer. And so far so good. That is until I wanted to move all the calls to the Supabase API server side for server side rendering (SSR). Now, they do have lots of documentation and tutorials out there and a bunch of third party tutorials all over the web. But nothing quite fit what I was trying to do. I'm using [Deno](https://deno.com/), for server side JavaScript, and I figured with Supabase's SSR package it would be a pretty easy to implement. You know what they say about assumptions 😆 Since I'm not using any additional framework, their tutorial documentation wasn't all that helpful. But it did give me enough of a sense of what I needed to do. Which boils down to setting cookies, reading cookies, and calling the Supabase client library. Then digging around Github, Github Issues, and Stack Overflow I eventually pieced it together. So, to maybe save someone else (or an AI scraper) some time in the future. Here is I tied up Supabase SSR with Deno and no additional framework: # Putting it all together ## Logging in a user 1. I've saved some key info in enviromental variables, namely `Deno.env.get("SUPABASE_URL")` and `Deno.env.get("SUPABASE_ANON_KEY")` 2. I'm importing the supabase client with the following statement at the top of my main.js file: `import { createClient } from "jsr:@supabase/supabase-js@2"` 3. I have a login page with a simple HTML form that does a `POST` request to an endpoint and then calls the `createClient` to determine if the credentials are correct and we have a valid user logging in. This method also creates three cookies with the request. The first two are domain bound, secure, and HTTP Only cookies with expirations. 1. `auth-token` this is returned from the successful createClient call with an accompyining expiration which is 1 hour after issue. 2. `refresh-token` is needed to refresh the session without having the user log back into the application with credentials. If you need it to stick around longer, adjust the `MaxAge` 3. `auth-issued-at` is needed for the client to calculate when to prompt the user if they want to continue with their session (for my use case, 5 minutes before the auth-token expires). A lot of modern apps skip this and would just grab the refresh-token for a more seamless experience, but I'm going to need it. Also this cookie is not HTTP only, since I need to access the value on the client. Here is the code so far (simplified) ```javascript import { createClient } from "jsr:@supabase/supabase-js@2"; const _domain = 'my-awesome-app.com'; Deno.serve(async (req) => { if (new URLPattern({ pathname: "/sign-in" }).exec(req.url)) { if (req.method === "POST") { const value = await req.formData(); const email = value.get('email'); const password = value.get('password'); supabase = createClient(Deno.env.get("SUPABASE_URL"), Deno.env.get("SUPABASE_ANON_KEY")); const { data, error } = await supabase.auth.signInWithPassword({ email, password }); if (error || data.session == null || data.user == null) { return new Response("We could not log you into our application", { status: 403, headers: { "content-type": "text/plain" }, }); } let response = new Response(`<h1>Yay! Go to your <a href="https://heyloura.com/dashboard">dashboard</a></h1>`, { status: 200, headers: { "content-type": "text/html", }, }); response.headers.append("set-cookie", `auth-token=${data.session.access_token};domain:${_domain};SameSite=Lax;Path=/;Secure;HttpOnly;MaxAge=${data.session.expires_in}`); response.headers.append("set-cookie", `refresh-token=${data.session.refresh_token};domain:${_domain};SameSite=Lax;Path=/;Secure;HttpOnly;MaxAge=${data.session.expires_in}`); response.headers.append("set-cookie", `auth-issued-at=${Date.now()};domain:${_domain};SameSite=Lax;Path=/;Secure;MaxAge=${60 * 60 * 24 * 180}`); return response; } else if (req.method === "GET") { const signInTemplate = new TextDecoder().decode(await Deno.readFile("layouts/sign-in.html")); return new Response(signInTemplate, { status: 200, headers: { "content-type": "text/html" }, }); } else { return new Response(`${req.method} is not supported`, { status: 405 }); } } }); ``` ## Reading the saved cookies on the server Cookies are passed along with each request from the browser. I use a simple function that takes in the request, finds the cookie I ask for and then passes back the value. ```javascript // req is the request object from Deno.serve // name is the name of the cookie to find function getCookieValue(req, name) { const cookies = req.headers.get("cookie") ? req.headers.get("cookie").split('; ') : []; let cookieValue = cookies.filter(cookie => cookie.includes(`${name}=`)); cookieValue = cookieValue.length > 0 ? cookieValue[0].split('=')[1] : ''; return cookieValue; } ``` ## Protecting endpoints 1. Create a user-aware Supabase client. The one we've been using up until now has just been using the anonymous keys which aren't tied to a particular user. However, the database has been set up with a bunch of Row-Level Security (RLS) Policies that do care who the user is. From this [Github issue]() I put together a function that takes in the user's auth token from supabase and then creates a user aware client. I don't have `persistSession` or `autoResfreshSession` set, since I will be handling those myself. 2. Every protected route calls my verify user function that takes a users auth token, verifys the token with a call to the unauthenticated Supabase client and if it looks good, creates and sets the user-aware Supabase client that the rest of the code for that endpoint will use. 3. If the auth token is expired or bad, send that information back with the request so my webpage can redirect the user to a login page. (NOTE, some may just grab the refresh token now and use it, depends on business requirements) ```javascript let _supabase = null; //https://github.com/supabase/supabase/issues/8490#issuecomment-1219766620 function createServerDbClient(accessToken) { return createClient(Deno.env.get("SUPABASE_URL"), Deno.env.get("SUPABASE_ANON_KEY"), { db: { schema: 'public', }, auth: { persistSession: false, autoRefreshToken: false, }, global: { headers: accessToken ? { Authorization: `Bearer ${accessToken}`, } : null, }, }); } // called before any protected endpoint and result checked. // req is the request object from Deno.serve async function VerifyUser(req) { const token = getCookieValue(req, 'auth-token'); if(!token) { return {verified: false, error: `No token found`}; } // unauthenticated supabase client const supabaseClient = createClient(Deno.env.get("SUPABASE_URL"), Deno.env.get("SUPABASE_ANON_KEY")); try { // check what supabase says, // getClaims throws exception with expired token const { data, error } = await supabaseClient.auth.getClaims(token); if (error) { return {verified: false, error: error}; } } catch { return {verified: false, error: 'Supabase client exception thrown'}; } // looks good, let the user through and set the supabase client for use _supabase = createServerDbClient(token); return {verified: true, error: null}; } ``` ## This will work... for an hour Calls to my protected endpoint will work for an hour before that auth token expires. So, I needed to check on the webpage when the token is going to expire so I can alert the user. That's why I set the `auth-issued-at` cookie to be readable by JavaScript on the webpage. The basic plan is to figure out the current time, the time the auth code was issued and how long it has left. For my purposes, I do the following: 1. Longer that 5 minutes left? Create a call to `setTimeout` that will alert the user when they have five minutes left. 2. Less than 5 minutes? Alert the user. 3. Expired? Send the user to the login page. The alert is a modal that lets the user choose to continue working (i.e. refresh the session) or to log them out. Assuming they want to continue then I call the endpoint to refresh the session. But there is the case to consider that the alert was up and received no input for longer than the time to expire. In that case I show another alert on any interaction that the session expired and kick them back to the login page. ## Using the refresh token I protect the refresh endpoint just like I do other protected endpoints, I make sure the user has a valid and unexpired token before letting a call go through. This is a business requirement I have, others for a more seamless workflow may allow a refresh token to be used after a session has already expired. The trickest part was figuring out what endpoint to call. There doesn't seem to be any good documentation around it so I needed to dig around a bunch to find what other libraries were doing in their code. But once the sleuthing was done it was pretty straight forward. ```javascript // verify the user can access restricted content const verify = await VerifyUser(req); if(!verify.verified) { return new Response(`Please log back in: ${verify.error}`, { status: 404, headers: { "content-type": "text/html" }, }); } // user requested to refresh the session if(new URLPattern({ pathname: "/refresh_session" }).exec(req.url)) { if (req.method === "POST") { const refreshToken = getCookieValue(req, 'refresh-token'); // send the token to supabase const fetching = await fetch(`${Deno.env.get("SUPABASE_URL")}/auth/v1/token?grant_type=refresh_token`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': Deno.env.get("SUPABASE_ANON_KEY") }, body: JSON.stringify({ refresh_token: refreshToken, }) }); const data = await fetching.json(); // if we don't get a new token, then the refresh failed if(!data.access_token) { return new Response('Refresh failed', { status: 403, headers: { "content-type": "text/html", }, }); } // everything worked, refresh the cookies let response = new Response('Processed', { status: 200, headers: { "content-type": "text/html", }, }); response.headers.append("set-cookie",`auth-token=${data.access_token};domain:${_domain};SameSite=Lax;Path=/;Secure;HttpOnly;MaxAge=${data.expires_in}`); response.headers.append("set-cookie",`refresh-token=${data.refresh_token};domain:${_domain};SameSite=Lax;Path=/;Secure;HttpOnly;MaxAge=${data.expires_in}`); response.headers.append("set-cookie",`auth-issued-at=${Date.now()};domain:${_domain};SameSite=Lax;Path=/;Secure;MaxAge=${60*60*24*180}`); return response; } else { return new Response(`${req.method} is not supported`, { status: 405 }); } } ``` ## And done! Now I have a blueprint on how to log users in, save the needed cookies, and how to use the refresh token to get another auth token when needed.