<?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: Ty North</title>
    <description>The latest articles on DEV Community by Ty North (@tynorth).</description>
    <link>https://dev.to/tynorth</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%2F254139%2F5a44104d-8bf2-48fe-9c99-a9b7e320a056.jpg</url>
      <title>DEV Community: Ty North</title>
      <link>https://dev.to/tynorth</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tynorth"/>
    <language>en</language>
    <item>
      <title>You Don't Need TypeScript. You Need Runtime Guarantees.</title>
      <dc:creator>Ty North</dc:creator>
      <pubDate>Mon, 16 Jun 2025 19:22:16 +0000</pubDate>
      <link>https://dev.to/tynorth/you-dont-need-typescript-you-need-runtime-guarantees-de0</link>
      <guid>https://dev.to/tynorth/you-dont-need-typescript-you-need-runtime-guarantees-de0</guid>
      <description>&lt;h3&gt;
  
  
  &lt;em&gt;Hot take: We're using the wrong tool for the job. Your TypeScript types are giving you a false sense of security where it matters most.&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;Let's be honest. We adopted TypeScript for safety. And for the code you write and control, it gets the job done.&lt;/p&gt;

&lt;p&gt;But where do the most dangerous bugs originate? From the outside world: API responses, user input, webhook payloads. Then here comes TypeScript to do absolutely nothing.&lt;/p&gt;

&lt;p&gt;That's right. TypeScript, a &lt;em&gt;compile-time&lt;/em&gt; tool, is completely silent when your app receives malformed JSON at &lt;em&gt;runtime&lt;/em&gt;. Your beautiful &lt;code&gt;type User = { id: number; }&lt;/code&gt; offers zero resistance when an API mistakenly sends &lt;code&gt;{ "id": "user-123" }&lt;/code&gt;. The code compiles, your app runs, and then it crashes.&lt;/p&gt;

&lt;p&gt;This fundamental gap forces us into a clumsy, defensive workflow that I call the "Two-Sources-of-Truth Trap."&lt;/p&gt;

&lt;p&gt;If writing the same logic twice doesn't make you itch, then you should get that checked out.&lt;/p&gt;

&lt;p&gt;In the meantime, let's look at a typical setup that lures us into the two sources of truth trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem in Code: The Two-Sources-of-Truth Trap
&lt;/h2&gt;

&lt;p&gt;To safely handle data from the outside world, you have to define your data shape in at least two places.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The Standard TypeScript Way&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. First, you define the TypeScript &lt;code&gt;type&lt;/code&gt; for your editor and compiler:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/types.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inactive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Then, you define a near-identical &lt;code&gt;schema&lt;/code&gt; for runtime validation using a library like Zod:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/schemas.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&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;inactive&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;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Finally, you use both:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./schemas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&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="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/user/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validatedUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&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;data&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;validatedUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works, but it's boilerplate. It's two sources of truth. Boooo....&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: A Single Source of Truth
&lt;/h2&gt;

&lt;p&gt;I decided to reject this duplication, as should y'all. So, I built &lt;strong&gt;AssertScript&lt;/strong&gt;, a tool based on a simple philosophy: your &lt;strong&gt;runtime validation schema should be the single source of truth&lt;/strong&gt;, from which everything else is generated.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The AssertScript Way&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. The Single Definition:&lt;/strong&gt;&lt;br&gt;
This &lt;code&gt;types.json&lt;/code&gt; file is the one and only contract for your data. It defines the types and the rules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;types.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"User"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^.+@.+&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;..+$"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"inactive"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. The Generation Step (Done once, or on save):&lt;/strong&gt;&lt;br&gt;
You run a command: &lt;code&gt;npm run generate:dts&lt;/code&gt;. This produces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A dependency-free &lt;code&gt;validators.js&lt;/code&gt; file for runtime enforcement.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;validators.d.ts&lt;/code&gt; file for editor intelligence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. The Usage (Lean and Zero-Dependency):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;validateUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./validators.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// The .d.ts file gives us full autocompletion below!&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/user/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="c1"&gt;// One function call to validate everything at runtime. No extra dependencies.&lt;/span&gt;
  &lt;span class="nf"&gt;validateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// If this line is reached, data is GUARANTEED to have the correct shape.&lt;/span&gt;
  &lt;span class="c1"&gt;// We can use it with 100% confidence and first-class editor support.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&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;There is no duplication. No production dependencies. The runtime validation &lt;em&gt;is&lt;/em&gt; the source of truth, and the editor support is a free, generated byproduct.&lt;/p&gt;

&lt;h2&gt;
  
  
  TLDR: Stop Asking Your Compiler to Do a Validator's Job
&lt;/h2&gt;

&lt;p&gt;We've been misapplying a great tool. TypeScript is fantastic for ensuring the code &lt;em&gt;you write&lt;/em&gt; is internally consistent. But it is, by design, the wrong tool for guaranteeing the integrity of data that comes from the outside world.&lt;/p&gt;

&lt;p&gt;Stop maintaining two sources of truth for your data models. Stop adding heavy dependencies to do a job that can be handled with a simple, generated function.&lt;/p&gt;

&lt;p&gt;Define your runtime contracts once, generate your guards, and build safer, leaner, and more reliable applications.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The full source code for the AssertScript generator is available on my GitHub: &lt;a href="https://github.com/TyNorth/AssertScript" rel="noopener noreferrer"&gt;AssertScript Repo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What do you think? Is the "TypeScript + Zod" pattern the best we can do, or is it time to rethink our approach to data validation? &lt;/p&gt;

&lt;p&gt;more to come...&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>So, I'm Building This Thing Called LoreStrom: Dev Journey</title>
      <dc:creator>Ty North</dc:creator>
      <pubDate>Fri, 30 May 2025 14:14:34 +0000</pubDate>
      <link>https://dev.to/tynorth/so-im-building-this-thing-called-lorelight-dev-journey-hho</link>
      <guid>https://dev.to/tynorth/so-im-building-this-thing-called-lorelight-dev-journey-hho</guid>
      <description>&lt;p&gt;Alright, so I have a confession. I'm pretty much addicted to building apps. It's just my thing. The idea for &lt;strong&gt;LoreStrom&lt;/strong&gt; just came to me one day – this vision of a space for &lt;strong&gt;100% human-crafted worlds and stories, made easily shareable by the creator.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This was right of the bat of building an AI-assisted writing platform (more on that later).&lt;/p&gt;

&lt;p&gt;I envisioned a place where you and I, as creators, could &lt;em&gt;really&lt;/em&gt; dive deep into building fictional universes – all those intricate histories, the quirky characters with their own sagas, the sprawling lore – and then offer readers a genuinely cool way to get lost in it all. Something that gives writers total creative freedom and readers a uniquely engaging portal.&lt;/p&gt;

&lt;p&gt;Now, you might be thinking, "Why LoreStrom?" You're right to ask this question! It's true that there are many apps out there to facilitate this kind of creativity. But I'm a creative, so I'm building the tool that I want. LoreStrom is the platform &lt;em&gt;I&lt;/em&gt; wanted to see (or will be).&lt;/p&gt;

&lt;p&gt;Being a teacher, and also having built &lt;a href="https://thewritersden.app" rel="noopener noreferrer"&gt;The Writer's Den&lt;/a&gt; (which, for context, is an AI-assisted tool for novelists), I've spent a lot of time pondering the tools we use to bring ideas to life. For LoreStrom, I felt a strong pull towards something different – a space that champions and amplifies pure human imagination, offering the canvas and the toolkit without an AI co-author. A place where  I, and others could pour their heart and souls into.&lt;/p&gt;

&lt;p&gt;So, this article? I just wanted to share my journey, expose LoreStrom shamelessly, and if you pick up a useful tip along the way, I'll be estactic.&lt;/p&gt;

&lt;p&gt;Ready?&lt;/p&gt;

&lt;p&gt;Let's go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Picking the Right Gear: My Tech Stack – Why These Tools?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every good project needs a solid foundation. Here’s what I chose for LoreStrom and why it felt right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend - Vue 3 &amp;amp; the Quasar Power-Up:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;For the frontend, I went with &lt;strong&gt;Vue 3&lt;/strong&gt;, mostly because the &lt;strong&gt;Composition API&lt;/strong&gt; is just fantastic for keeping complex components organized and readable.&lt;/li&gt;
&lt;li&gt;Then comes &lt;strong&gt;Quasar Framework&lt;/strong&gt;. Seriously, if you're doing Vue and not using Quasar, you &lt;strong&gt;are&lt;/strong&gt; working too hard! It's got everything you need including an awesome component library, and CLI. I ean seriously, Quasar deserves more attention. What truly matters is it let me build the UI fast and focus on the actual LoreStrom features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pinia&lt;/strong&gt; for state management and Vue Router for navigation are pretty standard in my Vue toolkit – they just work.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Backend - Supabase for the Win!&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;If you haven't played with &lt;strong&gt;Supabase&lt;/strong&gt; yet, let me give you the rundown: it's an open-source Firebase alternative built on &lt;strong&gt;PostgreSQL&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You get Authentication out of the box, instant APIs (REST and GraphQL), Storage for files, Edge Functions, Realtime stuff... the works. For LoreStrom, this meant I could get a powerful, scalable backend up and running &lt;em&gt;fast&lt;/em&gt; without wrestling with server configurations. The fact that it’s SQL-based is also a huge win for the kind of structured, relational data a world-building app needs.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Building the Bones: Core Features Taking Shape&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With the tools in hand, I started laying down the core structure of LoreStrom:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Universe as a Canvas:&lt;/strong&gt; It all starts with the "Universe." This is where creators get that complete freedom I was talking about. Define your cosmology, history, unique rules – go wild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Populating the Worlds:&lt;/strong&gt; Inside each Universe, you can then build out your &lt;strong&gt;Characters&lt;/strong&gt; (with all their quirks, backstories, and even visual portraits down the line), weave detailed &lt;strong&gt;Lore&lt;/strong&gt; entries, and, of course, write &lt;strong&gt;Stories&lt;/strong&gt; broken down into chapters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two Sides to Every Portal: Creator &amp;amp; Reader Modes:&lt;/strong&gt; It was important to have distinct experiences. Using Quasar's layouts and Vue's conditional rendering, it wasn't too tough to switch the UI from a feature-rich dashboard for creators to a clean, immersive interface for readers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Real Work: Battles Fought &amp;amp; Lessons Learned (My Supabase Saga)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where the real fun (and sometimes hair-pulling) begins. Building is one thing; making it work &lt;em&gt;reliably&lt;/em&gt; and &lt;em&gt;securely&lt;/em&gt; is another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The 'New User' Problem: "Welcome! Uh... Where's Your Profile?"&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Situation:&lt;/strong&gt; So, a user signs up with Supabase Auth. Cool, they're in &lt;code&gt;auth.users&lt;/code&gt;. But LoreStrom needs its &lt;em&gt;own&lt;/em&gt; profile table, &lt;code&gt;public.users&lt;/code&gt;, to store app-specific info like their chosen &lt;code&gt;username&lt;/code&gt;, &lt;code&gt;bio&lt;/code&gt;, &lt;code&gt;creator_enabled&lt;/code&gt; flag, etc. This &lt;code&gt;public.users&lt;/code&gt; record wasn't just magically appearing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My First Thoughts (and why they weren't the best):&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Client-Side Creation:&lt;/em&gt; "Easy," I thought, "after &lt;code&gt;supabase.auth.signUp()&lt;/code&gt; works, I'll just make another API call from the frontend to create their profile."

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;The Catch?&lt;/em&gt; What if their internet glitches? Or they close the tab too soon? We could end up with an authenticated user who has no actual app profile. Not good. Plus, handling RLS for that very first insert from a "new" user is a bit of a pain.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Best Practice &amp;amp; My "Aha!" Moment: Supabase Database Triggers&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;This was the answer. Instead of relying on the client, I told the database to handle it. I wrote a PostgreSQL function that automatically creates an entry in &lt;code&gt;public.users&lt;/code&gt; whenever a new user is added to &lt;code&gt;auth.users&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- This function runs automatically thanks to the trigger below&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;replace&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_new_user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="k"&gt;language&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt; &lt;span class="k"&gt;definer&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;
&lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;-- user_id is the foreign key to auth.users.id&lt;/span&gt;
  &lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_user_meta_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'username'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;-- 'username' is passed from client during signUp&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- This trigger calls the function whenever a new user is created in auth.users&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="n"&gt;on_auth_user_created&lt;/span&gt;
  &lt;span class="k"&gt;after&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;each&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;execute&lt;/span&gt; &lt;span class="k"&gt;procedure&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_new_user&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;em&gt;Why this is the way (for me, at least):&lt;/em&gt; It's atomic – happens server-side right when the auth user is created. It's reliable. And the &lt;code&gt;username&lt;/code&gt;? I just pass that from my registration form in the &lt;code&gt;options.data&lt;/code&gt; field of the &lt;code&gt;supabase.auth.signUp()&lt;/code&gt; call, and the trigger picks it up. Clean.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. RLS Mysteries: The Case of the &lt;code&gt;406 Not Acceptable&lt;/code&gt; Error&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Frustration:&lt;/strong&gt; Users were logged in, but when the app tried to fetch their profile from &lt;code&gt;public.users&lt;/code&gt;... &lt;code&gt;406 Not Acceptable&lt;/code&gt;. My query looked right, the JWT was being sent. What was Supabase trying to tell me?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Culprit: Row Level Security (RLS).&lt;/strong&gt; If you're working with Supabase and user data, RLS is your best friend for security, but you &lt;em&gt;have&lt;/em&gt; to set it up. It lets you define &lt;em&gt;exactly&lt;/em&gt; who can see or change which &lt;em&gt;rows&lt;/em&gt; in a table. By default, if RLS is on, &lt;em&gt;nobody&lt;/em&gt; can do &lt;em&gt;anything&lt;/em&gt; until you write a policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Troubleshooting &amp;amp; The Fix:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Dev Tip:&lt;/em&gt; First thing I did was temporarily toggle RLS &lt;em&gt;off&lt;/em&gt; for the &lt;code&gt;public.users&lt;/code&gt; table in the Supabase dashboard. The query worked! That told me 100% it was an RLS policy issue. (Pro tip: always remember to turn RLS back on!).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Best Practice &amp;amp; Our Solution: Granular Policies&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;We needed users to read &lt;em&gt;their own&lt;/em&gt; profile. So, the &lt;code&gt;SELECT&lt;/code&gt; policy became:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Allow individual read access to own profile"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt; &lt;span class="c1"&gt;-- This policy applies to any logged-in user&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;-- They can only select rows where their auth ID matches the table's user_id column&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Why this is key:&lt;/em&gt; It enforces the principle of least privilege. Users get access &lt;em&gt;only&lt;/em&gt; to what they need. We set up a similar one for &lt;code&gt;UPDATE&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. State of Confusion: When Your App &amp;amp; Guard Disagree&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Head-Scratcher:&lt;/strong&gt; My UI would show a "Login" button (because &lt;code&gt;userStore.profile&lt;/code&gt; was &lt;code&gt;null&lt;/code&gt; – often due to the RLS issue above), but clicking it did nothing! Or, if I typed &lt;code&gt;/auth/login&lt;/code&gt; manually, I'd just get bounced back to the homepage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Diagnosis:&lt;/strong&gt; My &lt;code&gt;userStore&lt;/code&gt; &lt;em&gt;did&lt;/em&gt; have an &lt;code&gt;authUser&lt;/code&gt; (Supabase knew I had a session), so &lt;code&gt;userStore.isLoggedIn&lt;/code&gt; was true. But because &lt;code&gt;userStore.profile&lt;/code&gt; wasn't loading, my UI (checking &lt;code&gt;userStore.profile&lt;/code&gt;) thought I &lt;em&gt;wasn't&lt;/em&gt; logged in and showed the "Login" button. My Vue Router navigation guard, however, correctly saw &lt;code&gt;isLoggedIn&lt;/code&gt; as true and blocked access to the login page, sending me home. A classic state mismatch!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Workaround vs. The Real Fix:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;My quick fix was &lt;code&gt;await userStore.logout()&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; &lt;code&gt;router.push('/auth/login')&lt;/code&gt; in the login button's click handler. It forced the state to "logged out" so the guard would let me through.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;The Proper Solution:&lt;/em&gt; The real fix was ensuring that my &lt;code&gt;userStore.initialize()&lt;/code&gt; action not only set &lt;code&gt;authUser&lt;/code&gt; but also &lt;em&gt;reliably awaited&lt;/em&gt; the &lt;code&gt;fetchProfile&lt;/code&gt; call. Once the RLS issue was sorted and &lt;code&gt;fetchProfile&lt;/code&gt; worked, &lt;code&gt;userStore.profile&lt;/code&gt; got populated, and the UI, the store, and the navigation guard were all singing from the same hymn sheet. Consistent state is everything!&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;LoreStrom's Next Chapter: What I'm Building Now &amp;amp; Dreaming Up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The core is taking shape, but there's always more to build!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual Richness with Supabase Storage:&lt;/strong&gt; Next up is integrating Supabase Storage. Creators need to be able to upload those cool universe banners, character portraits, and other visuals to really make their worlds shine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio Scenes – Hear the Story:&lt;/strong&gt; I'm super excited about this idea. Imagine reading a story and being able to trigger short audio clips – a character's key line of dialogue, ambient sounds of a bustling fantasy market, a dramatic musical sting. It’s about adding another layer for writers to tell their story and for readers to experience it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Visual Storytelling:&lt;/strong&gt; I've been really inspired by interactive experiences like Google Spotlight Stories. The aim isn't to turn LoreStrom into a game engine, but to explore how we can present text and art more dynamically, making the reading experience itself more of an event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Marketplace for Creators (The Big Dream):&lt;/strong&gt; Looking further out, I'd love to build a marketplace where creators could optionally offer paid access to their premium universes, exclusive story arcs, or perhaps early access to content. This could provide a way for them to earn from their incredible creativity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My Stance on AI (for LoreStrom):&lt;/strong&gt; As I mentioned, I built &lt;code&gt;thewritersden.app&lt;/code&gt;, which is all about AI-assisted novel writing. I'm fascinated by AI. But for LoreStrom, the philosophy is different. This platform is deliberately a space for &lt;strong&gt;100% human-crafted&lt;/strong&gt; worlds and stories. It's about providing the best possible canvas and tools for an author's unique vision, not an AI co-writer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Wrapping Up: The Adventure Continues&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building LoreStrom has been (and continues to be) an awesome ride – a fantastic mix of creative design and tough coding challenges. It really drives home that old saying: the best way to learn is by building, especially when it’s something you’re truly passionate about creating because &lt;em&gt;you&lt;/em&gt; wish it existed.&lt;/p&gt;

&lt;p&gt;I hope sharing some of these "behind-the-scenes" moments from my dev journey has been interesting or maybe even helpful for your own projects. If you've tackled similar stuff or just want to chat about world-building and code, I'm all ears!&lt;/p&gt;

&lt;p&gt;If you're interested in checking out LoreStrom, you &lt;strong&gt;can&lt;/strong&gt; do so &lt;a href="https://lorestrom.web.app/#/" rel="noopener noreferrer"&gt;Here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Until next time, keep coding away!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>saas</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
