<?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: Fredy Sandoval</title>
    <description>The latest articles on DEV Community by Fredy Sandoval (@fredysandoval).</description>
    <link>https://dev.to/fredysandoval</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F889622%2Fef5b2f52-9cc2-4663-873d-523c6862df45.png</url>
      <title>DEV Community: Fredy Sandoval</title>
      <link>https://dev.to/fredysandoval</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fredysandoval"/>
    <language>en</language>
    <item>
      <title>Lessons from Astro Starlight: Designing a Maintainable i18next Architecture</title>
      <dc:creator>Fredy Sandoval</dc:creator>
      <pubDate>Tue, 25 Aug 2026 14:25:04 +0000</pubDate>
      <link>https://dev.to/fredysandoval/lessons-from-astro-starlight-designing-a-maintainable-i18next-architecture-3hmm</link>
      <guid>https://dev.to/fredysandoval/lessons-from-astro-starlight-designing-a-maintainable-i18next-architecture-3hmm</guid>
      <description>&lt;p&gt;Astro Starlight is a useful reference implementation for internationalization because it does not treat &lt;code&gt;i18next&lt;/code&gt; as the entire i18n system.&lt;/p&gt;

&lt;p&gt;Instead, Starlight uses several layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the application/framework determines &lt;strong&gt;which locale is active&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;translation files are treated as &lt;strong&gt;validated application data&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;multiple translation sources are merged according to an explicit precedence;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;i18next&lt;/code&gt; provides the actual translation engine;&lt;/li&gt;
&lt;li&gt;consumers receive a small, locale-bound &lt;code&gt;t()&lt;/code&gt; API;&lt;/li&gt;
&lt;li&gt;TypeScript and schemas make invalid translation usage harder;&lt;/li&gt;
&lt;li&gt;plugins and third-party UI systems participate in the same translation pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation is the main lesson worth carrying into Astro, React, Next.js, server-rendered applications, component libraries, and other projects.&lt;/p&gt;

&lt;p&gt;Starlight currently depends on &lt;code&gt;i18next&lt;/code&gt; directly and creates its own i18next instance rather than using the package-global singleton.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Separate locale resolution from translation lookup
&lt;/h2&gt;

&lt;p&gt;A common i18next implementation starts by asking i18next to detect the user's language.&lt;/p&gt;

&lt;p&gt;Starlight takes a different approach.&lt;/p&gt;

&lt;p&gt;Astro/Starlight first resolves the locale from the application's routing configuration. That locale is then passed into the translation system. Middleware creates a translation function for &lt;code&gt;context.currentLocale&lt;/code&gt; and exposes it to the rest of the request as &lt;code&gt;Astro.locals.t&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request
   ↓
router / application locale resolution
   ↓
locale = "fr"
   ↓
translation service
   ↓
t = getTranslator("fr")
   ↓
component
   ↓
t("navigation.home")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is preferable to making i18next simultaneously responsible for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;URL routing
cookie handling
browser detection
locale selection
translation loading
translation lookup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are separate responsibilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  General lesson
&lt;/h3&gt;

&lt;p&gt;Design an application-level locale resolver first.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&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="s1"&gt;fr&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="s1"&gt;es&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Locale&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Depending on your application:&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. URL&lt;/span&gt;
  &lt;span class="c1"&gt;// 2. authenticated user preference&lt;/span&gt;
  &lt;span class="c1"&gt;// 3. cookie&lt;/span&gt;
  &lt;span class="c1"&gt;// 4. Accept-Language&lt;/span&gt;
  &lt;span class="c1"&gt;// 5. default locale&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then make translation lookup explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTranslator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a React SPA, browser-language detection may still be appropriate. The important part is that locale resolution should have a clearly defined owner rather than emerging accidentally from several libraries.&lt;/p&gt;

&lt;p&gt;For SSR applications, this separation is particularly useful because the server already knows which locale is rendering the request.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Treat translation dictionaries as application data
&lt;/h2&gt;

&lt;p&gt;Starlight does not simply import arbitrary JSON and hope that keys are correct.&lt;/p&gt;

&lt;p&gt;Its &lt;code&gt;i18n&lt;/code&gt; collection has a Zod schema. The schema describes recognized translation keys and even attaches descriptions explaining what each string represents.&lt;/p&gt;

&lt;p&gt;Examples include concepts such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search.label
themeSelect.dark
languageSelect.accessibleLabel
tableOfContents.onThisPage
page.previousLink
aside.warning
heading.anchorLabel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important idea is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;translations are data with a contract
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;translations are arbitrary Record&amp;lt;string, string&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;p&gt;A schema can detect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invalid value types;&lt;/li&gt;
&lt;li&gt;misspelled keys;&lt;/li&gt;
&lt;li&gt;unsupported structures;&lt;/li&gt;
&lt;li&gt;malformed generated catalogs;&lt;/li&gt;
&lt;li&gt;accidental translation metadata;&lt;/li&gt;
&lt;li&gt;plugin-specific extensions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It can also become a source for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;editor autocomplete;&lt;/li&gt;
&lt;li&gt;translation documentation;&lt;/li&gt;
&lt;li&gt;automated catalog validation;&lt;/li&gt;
&lt;li&gt;translation-management tooling;&lt;/li&gt;
&lt;li&gt;generated TypeScript declarations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recommended pattern
&lt;/h3&gt;

&lt;p&gt;Maintain a canonical schema beside your i18n infrastructure:&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;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="s1"&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;uiMessagesSchema&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigation.home&lt;/span&gt;&lt;span class="dl"&gt;'&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigation.settings&lt;/span&gt;&lt;span class="dl"&gt;'&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search.label&lt;/span&gt;&lt;span class="dl"&gt;'&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search.results&lt;/span&gt;&lt;span class="dl"&gt;'&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;account.signOut&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A more mature system can store translator context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigation.home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Primary navigation link to the application home page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;account.signOut&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Button used to terminate the current authenticated session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translator context is not cosmetic. A translator seeing only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"open": "Open"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does not know whether "open" is a verb, adjective, menu state, button, or accessibility label.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Use different validation strictness for canonical and override catalogs
&lt;/h2&gt;

&lt;p&gt;Starlight makes an interesting distinction.&lt;/p&gt;

&lt;p&gt;Its bundled Starlight strings are validated against a stricter schema, while the schema exposed to application users is partial and extensible. This lets a project provide only the translations it wants to override rather than having to reproduce the complete Starlight catalog.&lt;/p&gt;

&lt;p&gt;This suggests a strong general pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Canonical/default locale
&lt;/h3&gt;

&lt;p&gt;Require everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;defaultCatalogSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;uiMessagesSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A missing English source string should fail CI or the build.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secondary locales
&lt;/h3&gt;

&lt;p&gt;Depending on your translation workflow, either require completeness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;localeCatalogSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;uiMessagesSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or deliberately allow incremental translation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;localeCatalogSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;uiMessagesSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with runtime fallback to the default locale.&lt;/p&gt;

&lt;p&gt;The decision should be explicit.&lt;/p&gt;

&lt;p&gt;Do not accidentally allow partial translation simply because JSON files are unchecked.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Establish an explicit translation precedence
&lt;/h2&gt;

&lt;p&gt;Starlight combines three categories of translations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Starlight built-ins
       ↓
plugin translations
       ↓
project/user translations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resource builder processes them in that order, so later sources can override earlier ones. Undefined or missing entries do not erase an earlier translation.&lt;/p&gt;

&lt;p&gt;This produces a clear policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;framework defaults &amp;lt; extension defaults &amp;lt; application overrides
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a highly reusable pattern.&lt;/p&gt;

&lt;p&gt;For a component platform, the equivalent might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;design-system defaults
        ↓
feature-package translations
        ↓
application translations
        ↓
customer/tenant overrides
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Implement the policy explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildCatalog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;core&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Messages&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;core&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For complex systems, prefer a merge function that can distinguish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;missing
undefined
empty string
null
intentional deletion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starlight itself ignores falsy translation values while merging. That behavior is worth understanding rather than copying blindly.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Give extensions a translation registration API
&lt;/h2&gt;

&lt;p&gt;Starlight plugins do not need access to the internals of the i18next resource store.&lt;/p&gt;

&lt;p&gt;Instead, plugins participate in an &lt;code&gt;i18n:setup&lt;/code&gt; phase and call:&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="nf"&gt;injectTranslations&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myPlugin.doThing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Do the thing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myPlugin.doThing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Faire le truc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those strings become available through the same translation API as Starlight's built-in strings.&lt;/p&gt;

&lt;p&gt;This is a useful architecture for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plugins;&lt;/li&gt;
&lt;li&gt;feature modules;&lt;/li&gt;
&lt;li&gt;packages in a monorepo;&lt;/li&gt;
&lt;li&gt;design systems;&lt;/li&gt;
&lt;li&gt;microfrontends;&lt;/li&gt;
&lt;li&gt;third-party integrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of allowing every feature to initialize i18next independently, define an extension contract.&lt;/p&gt;

&lt;p&gt;For example:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TranslationExtension&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;namespace&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;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;registerTranslations&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;billing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;billing.invoice.download&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Download invoice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;billing.invoice.download&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Télécharger la facture&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starlight's plugin examples use keys prefixed with the plugin name, such as &lt;code&gt;myPlugin.doThing&lt;/code&gt;, which is a practical convention for avoiding collisions.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Hide the raw i18next instance behind an application API
&lt;/h2&gt;

&lt;p&gt;Starlight creates a dedicated i18next instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i18n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i18next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createInstance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fallbackLng&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;It then returns locale-bound translators using &lt;code&gt;getFixedT()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;i18next itself documents &lt;code&gt;createInstance()&lt;/code&gt; for separate i18next instances and &lt;code&gt;getFixedT()&lt;/code&gt; for generating a translation function bound to a language or namespace.&lt;/p&gt;

&lt;p&gt;This means most Starlight components do not care about initialization.&lt;/p&gt;

&lt;p&gt;They receive something conceptually equivalent to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTranslator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search.label&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  General lesson
&lt;/h3&gt;

&lt;p&gt;Your application code should depend primarily on:&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="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;not:&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="nx"&gt;i18next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;changeLanguage&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="nx"&gt;i18next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addResourceBundle&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="nx"&gt;i18next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="nx"&gt;i18next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resourceStore&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep those operations in the infrastructure layer.&lt;/p&gt;

&lt;p&gt;A useful API might be:&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;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Translator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TranslationKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;TranslationOptions&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="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TranslationKey&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ltr&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="s1"&gt;rtl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Readonly&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;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;Starlight adds exactly this kind of convenience around i18next: its translator exposes &lt;code&gt;t()&lt;/code&gt;, &lt;code&gt;t.exists()&lt;/code&gt;, &lt;code&gt;t.dir()&lt;/code&gt;, and &lt;code&gt;t.all()&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Bind translators to the request or rendering context
&lt;/h2&gt;

&lt;p&gt;Starlight does not continually mutate one global current language.&lt;/p&gt;

&lt;p&gt;Instead, it uses &lt;code&gt;getFixedT(locale, namespace)&lt;/code&gt; to obtain a translator for a particular locale. Middleware attaches that translator to the current request.&lt;/p&gt;

&lt;p&gt;This is especially relevant for SSR.&lt;/p&gt;

&lt;p&gt;A global mutable language can create problems when a server handles concurrent requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request A → French
request B → Japanese
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both mutate a shared singleton's active language, isolation becomes important.&lt;/p&gt;

&lt;p&gt;A safer server-side model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTranslator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestLocale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or an i18next instance scoped appropriately for the request/application architecture.&lt;/p&gt;

&lt;p&gt;For React SSR, &lt;code&gt;react-i18next&lt;/code&gt; also documents passing an i18next instance through &lt;code&gt;I18nextProvider&lt;/code&gt;, including request-specific instances for SSR scenarios.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Make fallback behavior part of the product policy
&lt;/h2&gt;

&lt;p&gt;Starlight explicitly sets its default locale as the &lt;code&gt;fallbackLng&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its documentation also treats fallback as a product feature: untranslated documentation pages can fall back to content in the default language instead of simply disappearing.&lt;/p&gt;

&lt;p&gt;The translation equivalent is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requested locale
       ↓
less-specific locale if applicable
       ↓
application default locale
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;i18next has built-in language-variant fallback behavior. For example, a regional locale can fall back to its broader language before the configured fallback language.&lt;/p&gt;

&lt;p&gt;A general application should therefore document fallback rules explicitly:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_LOCALE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&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;supportedLocales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-GB&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fr-CA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not let locale fallback be whatever i18next happens to do by default. i18next's default fallback is &lt;code&gt;dev&lt;/code&gt;; its documentation recommends explicitly selecting a real fallback language for production applications.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Use proper locale identifiers
&lt;/h2&gt;

&lt;p&gt;Starlight uses BCP-47 language tags and relies on platform internationalization APIs such as &lt;code&gt;Intl.Locale&lt;/code&gt; and &lt;code&gt;Intl.DisplayNames&lt;/code&gt; to derive information about locales. It also exposes language direction through the translation API.&lt;/p&gt;

&lt;p&gt;Prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;en
en-US
en-GB
pt-BR
zh-CN
zh-TW
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than application-specific formats such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;english
en_US
brazilian
zh_chinese
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BCP-47-compatible identifiers cooperate better with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Intl&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;browsers;&lt;/li&gt;
&lt;li&gt;HTTP language headers;&lt;/li&gt;
&lt;li&gt;i18next;&lt;/li&gt;
&lt;li&gt;translation vendors;&lt;/li&gt;
&lt;li&gt;date/number formatting APIs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One Starlight implementation detail should &lt;strong&gt;not&lt;/strong&gt; necessarily be copied: its helper for locating a bundled base-language translation uses a relatively simple region-stripping function. A general-purpose application with scripts and complex language variants should rely on proper locale negotiation rather than inventing BCP-47 parsing.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Make RTL direction part of the i18n service
&lt;/h2&gt;

&lt;p&gt;Internationalization is not only translated strings.&lt;/p&gt;

&lt;p&gt;Starlight exposes:&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="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and uses that result to produce &lt;code&gt;ltr&lt;/code&gt; or &lt;code&gt;rtl&lt;/code&gt;. Its locale configuration also includes writing direction.&lt;/p&gt;

&lt;p&gt;A general application should make direction available globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AppShell&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      ...
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;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;CSS should then favor logical properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;margin-inline-start&lt;/span&gt;
&lt;span class="nt"&gt;padding-inline-end&lt;/span&gt;
&lt;span class="nt"&gt;border-inline-start&lt;/span&gt;
&lt;span class="nt"&gt;inset-inline-end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of building separate RTL styles around &lt;code&gt;left&lt;/code&gt; and &lt;code&gt;right&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Keep translation keys stable and semantic
&lt;/h2&gt;

&lt;p&gt;Starlight's catalogs use keys such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search.label
themeSelect.dark
tableOfContents.onThisPage
page.previousLink
heading.anchorLabel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than using the English text itself as the identifier.&lt;/p&gt;

&lt;p&gt;This allows copy to change without changing the application's API.&lt;/p&gt;

&lt;p&gt;Prefer:&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="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout.payment.submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;over:&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="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pay now&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A practical naming structure is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;domain.component.meaning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auth.login.submit
auth.login.invalidPassword

billing.invoice.download
billing.invoice.status.overdue

navigation.account.settings
navigation.account.signOut
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For packages or plugins, prefix keys with the package identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;analytics.export.csv
editor.toolbar.bold
myPlugin.settings.title
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  12. Use interpolation for dynamic values, not sentence construction
&lt;/h2&gt;

&lt;p&gt;Starlight exposes normal i18next interpolation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"heading.anchorLabel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Section titled “{{title}}”"&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;and custom application strings can similarly use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"link.astro.custom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Astro documentation for {{feature}}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value is supplied through the &lt;code&gt;t()&lt;/code&gt; options object.&lt;/p&gt;

&lt;p&gt;Use interpolation when a value is genuinely dynamic:&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="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;welcome.user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;files.count&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid constructing sentences from fragments:&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="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;youHave&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;messages&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;i18next's own best-practices documentation warns that excessive interpolation and sentence fragmentation create localization problems because languages do not share the same grammar or word ordering.&lt;/p&gt;

&lt;p&gt;Pluralization should similarly remain inside the translation system rather than application conditionals. i18next's plural rules use &lt;code&gt;Intl.PluralRules&lt;/code&gt; and the &lt;code&gt;count&lt;/code&gt; option.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Integrate third-party UI into the same translation pipeline
&lt;/h2&gt;

&lt;p&gt;One particularly useful Starlight pattern appears in search.&lt;/p&gt;

&lt;p&gt;Pagefind is a separate library with its own UI translation API. Rather than creating an entirely separate locale system, Starlight extracts &lt;code&gt;pagefind.*&lt;/code&gt; entries from its central translation dictionary, removes the prefix, and passes them to Pagefind.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchTranslations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;searchProvider.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;searchProvider.&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="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;thirdPartySearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;translations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;searchTranslations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an excellent general rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The application owns localization. Third-party widgets should consume translations from the application's localization pipeline whenever possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That prevents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;main application locale = French
date picker locale = English
search UI locale = browser language
editor locale = previously saved setting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;from becoming four independent systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  14. Make translation keys part of the TypeScript API
&lt;/h2&gt;

&lt;p&gt;Starlight augments i18next's &lt;code&gt;CustomTypeOptions&lt;/code&gt; so the &lt;code&gt;starlight&lt;/code&gt; namespace knows its translation keys. User schema keys and plugin keys are also incorporated into the translation type.&lt;/p&gt;

&lt;p&gt;Plugin translations are particularly interesting: Starlight collects injected keys and generates a TypeScript declaration file in the consuming project so those keys become part of the typed translation API.&lt;/p&gt;

&lt;p&gt;The goal is:&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="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigation.home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// valid&lt;/span&gt;
&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigaton.home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// TypeScript error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of discovering the typo in production.&lt;/p&gt;

&lt;p&gt;i18next officially supports TypeScript resource typing through &lt;code&gt;CustomTypeOptions&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For a normal application, a simpler approach is often sufficient:&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="nx"&gt;en&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;./locales/en/common.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i18next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CustomTypeOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;defaultNS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nl"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;common&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a large modular project, generate those declarations from your translation registry or source locale.&lt;/p&gt;




&lt;h2&gt;
  
  
  15. File structure: what to copy and what to adapt
&lt;/h2&gt;

&lt;p&gt;Starlight itself keeps bundled UI translations as one file per locale and supports project translations from &lt;code&gt;src/content/i18n/*.json&lt;/code&gt;, &lt;code&gt;.yaml&lt;/code&gt;, or &lt;code&gt;.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A small application can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  i18n/
    index.ts
    config.ts
    schema.ts
    locales/
      en.json
      fr.json
      es.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a larger React application, namespaces usually scale better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  i18n/
    config.ts
    createI18n.ts
    resolveLocale.ts
    types.ts
    schemas/
      common.ts
      billing.ts
      editor.ts

    locales/
      en/
        common.json
        billing.json
        editor.json

      fr/
        common.json
        billing.json
        editor.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starlight uses one &lt;code&gt;starlight&lt;/code&gt; i18next namespace because its UI catalog is relatively cohesive. i18next explicitly recommends multiple namespaces when catalogs become large, belong to different semantic areas, or should be lazy-loaded separately.&lt;/p&gt;

&lt;p&gt;Therefore, the lesson is &lt;strong&gt;not&lt;/strong&gt; "always use one namespace."&lt;/p&gt;

&lt;p&gt;The lesson is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;choose namespace boundaries deliberately
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  16. A generalized architecture
&lt;/h2&gt;

&lt;p&gt;A reusable i18next architecture based on Starlight's strongest ideas looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 ┌─────────────────────────┐
                 │    Locale Resolver      │
                 │ URL / user / cookie /   │
                 │ Accept-Language         │
                 └────────────┬────────────┘
                              │
                              ▼
                 ┌─────────────────────────┐
                 │      Locale Config      │
                 │ supported / default /   │
                 │ BCP-47 / direction      │
                 └────────────┬────────────┘
                              │
                              ▼
    ┌─────────────┐   ┌───────────────────┐   ┌─────────────┐
    │ Core        │   │ Feature / plugin  │   │ Application │
    │ catalogs    │   │ catalogs          │   │ overrides   │
    └──────┬──────┘   └─────────┬─────────┘   └──────┬──────┘
           │                    │                    │
           └────────────────────┼────────────────────┘
                                ▼
                    ┌─────────────────────┐
                    │ Schema validation   │
                    │ + type generation   │
                    └──────────┬──────────┘
                               ▼
                    ┌─────────────────────┐
                    │ Resource assembly   │
                    │ explicit precedence │
                    └──────────┬──────────┘
                               ▼
                    ┌─────────────────────┐
                    │ private i18next     │
                    │ instance            │
                    └──────────┬──────────┘
                               ▼
                    ┌─────────────────────┐
                    │ getTranslator(lang) │
                    └──────────┬──────────┘
                               │
                ┌──────────────┼──────────────┐
                ▼              ▼              ▼
             React UI       server code    third-party UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This architecture remains useful even when none of the consuming code uses Astro.&lt;/p&gt;




&lt;h2&gt;
  
  
  17. Recommended React implementation
&lt;/h2&gt;

&lt;p&gt;For React, retain the architecture but replace &lt;code&gt;Astro.locals.t&lt;/code&gt; with React context through &lt;code&gt;react-i18next&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A reasonable initialization layer is:&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;// i18n/createI18n.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;i18next&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;i18next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initReactI18next&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;react-i18next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buildResources&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;./resources&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defaultLocale&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;./config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&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;createAppI18n&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i18next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createInstance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initReactI18next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fallbackLng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;defaultLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildResources&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;defaultNS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

      &lt;span class="na"&gt;interpolation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;escapeValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;instance&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;Then at the React boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;I18nextProvider&lt;/span&gt; &lt;span class="na"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;I18nextProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SaveButton&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTranslation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;actions.save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;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;code&gt;react-i18next&lt;/code&gt;'s &lt;code&gt;useTranslation()&lt;/code&gt; internally supplies a namespace-bound &lt;code&gt;t&lt;/code&gt; function using i18next's &lt;code&gt;getFixedT()&lt;/code&gt; mechanism, which is conceptually close to the API Starlight builds itself.&lt;/p&gt;

&lt;p&gt;For SSR, create or select the appropriate instance for the request rather than assuming a browser-global mutable language. &lt;code&gt;I18nextProvider&lt;/code&gt; is explicitly designed to support supplied instances and SSR use cases.&lt;/p&gt;




&lt;h2&gt;
  
  
  18. Suggested translation pipeline
&lt;/h2&gt;

&lt;p&gt;A production pipeline can be defined as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Developer adds or changes a source-language key.

2. Schema/type generation updates the canonical key contract.

3. CI validates the default catalog.

4. CI validates every locale:
   - valid keys
   - valid value types
   - interpolation placeholders
   - missing/extra keys according to project policy

5. Translation files are sent to translators or a localization platform.

6. Returned catalogs are validated again.

7. Build assembles:
   core
   + packages/plugins
   + application overrides.

8. i18next receives normalized resources.

9. Server/router determines the active locale.

10. Components receive a locale-bound translation API.

11. Third-party UI gets translations from that same API.

12. Automated tests verify fallback and representative locales.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main architectural principle is that i18next should sit &lt;strong&gt;inside&lt;/strong&gt; this pipeline rather than &lt;em&gt;be&lt;/em&gt; the pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  19. Tests worth adding
&lt;/h2&gt;

&lt;p&gt;An i18n implementation should test infrastructure, not every sentence.&lt;/p&gt;

&lt;p&gt;High-value tests include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Default locale contains every required key.

Supported locales contain no unknown keys.

Required locales meet the chosen completeness threshold.

Interpolated placeholders match between source and translation.

Plural forms work for representative languages.

Regional locale fallback works.

Application overrides beat package defaults.

Plugin/feature translations are registered.

Missing translation falls back to the configured locale.

RTL locales return rtl.

Unknown translation keys fail TypeScript checks.

Server requests for different locales remain isolated.

Third-party widgets receive the correct current-locale dictionary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starlight itself tests content collections and can mock i18n collection entries during tests, reinforcing the idea that translation catalogs are part of the application's data layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  20. What specifically should be copied from Starlight?
&lt;/h2&gt;

&lt;p&gt;The most transferable Starlight decisions are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Starlight decision&lt;/th&gt;
&lt;th&gt;General lesson&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Astro resolves the locale&lt;/td&gt;
&lt;td&gt;Keep locale resolution outside the translation engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;i18next.createInstance()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Own an application-specific i18next instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getFixedT(locale, namespace)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Give consumers locale-bound translators&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zod i18n schema&lt;/td&gt;
&lt;td&gt;Validate translations as structured data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strict bundled schema&lt;/td&gt;
&lt;td&gt;Make canonical catalogs complete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial user schema&lt;/td&gt;
&lt;td&gt;Allow controlled incremental overrides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in → plugin → user merging&lt;/td&gt;
&lt;td&gt;Define translation precedence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin &lt;code&gt;injectTranslations()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Give modules an extension API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generated plugin key types&lt;/td&gt;
&lt;td&gt;Make translation keys part of the TS contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Astro.locals.t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Expose one simple translation API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;t.exists()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Support capability/optional-key checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;t.dir()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Treat writing direction as first-class i18n data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;t.all()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allow integration with external UI libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pagefind.*&lt;/code&gt; translation forwarding&lt;/td&gt;
&lt;td&gt;Centralize localization even for third parties&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BCP-47 locale tags&lt;/td&gt;
&lt;td&gt;Use interoperable language identifiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit fallback locale&lt;/td&gt;
&lt;td&gt;Make missing-translation behavior predictable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  21. What should not be copied literally?
&lt;/h2&gt;

&lt;p&gt;Several Starlight decisions are appropriate for Starlight but should remain project-specific.&lt;/p&gt;

&lt;h3&gt;
  
  
  One namespace
&lt;/h3&gt;

&lt;p&gt;Starlight uses one &lt;code&gt;starlight&lt;/code&gt; namespace. Large applications may benefit from &lt;code&gt;common&lt;/code&gt;, &lt;code&gt;account&lt;/code&gt;, &lt;code&gt;billing&lt;/code&gt;, &lt;code&gt;editor&lt;/code&gt;, and other namespaces for ownership and lazy loading. i18next supports namespaces specifically for this purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Eagerly assembled resources
&lt;/h3&gt;

&lt;p&gt;Starlight's UI catalog is small enough to assemble into resources up front. A large client application may want lazy loading by language and namespace.&lt;/p&gt;

&lt;h3&gt;
  
  
  Astro content collections
&lt;/h3&gt;

&lt;p&gt;The useful concept is schema-validated catalogs, not Astro's particular loader implementation. React projects can reproduce this with Zod, JSON Schema, custom build scripts, or localization-platform validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Astro middleware
&lt;/h3&gt;

&lt;p&gt;The useful concept is request-bound translation state. Next.js middleware, Express request state, Remix loaders, React Router, or another request context can provide the equivalent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flat dotted keys
&lt;/h3&gt;

&lt;p&gt;Starlight's dotted flat keys work well for its catalog. A project can choose flat or nested resources, but the convention should be consistent and reflected in schema/types.&lt;/p&gt;




&lt;h2&gt;
  
  
  22. Recommended baseline for a new project
&lt;/h2&gt;

&lt;p&gt;For a new TypeScript/React project, a Starlight-inspired baseline would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Locale:
  use BCP-47 identifiers

Locale ownership:
  application/router resolves locale

Default:
  configure one explicit production fallback locale

Catalogs:
  JSON/YAML/TS resources organized by locale + namespace

Schema:
  validate every catalog during CI/build

Source locale:
  required and complete

Secondary locales:
  complete or deliberately partial

Keys:
  semantic, stable, domain-prefixed

Extensions:
  provide registerTranslations()/injectTranslations()

Precedence:
  platform &amp;lt; feature/package &amp;lt; application

Runtime:
  private i18next instance

React:
  I18nextProvider + useTranslation()

SSR:
  request-safe locale/instance handling

TypeScript:
  typed translation keys

RTL:
  propagate lang and dir to document root

Third parties:
  derive their dictionaries from the central catalog

Testing:
  schema, fallback, placeholders, plurals, overrides, RTL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;The biggest lesson from Starlight is not a particular i18next option.&lt;/p&gt;

&lt;p&gt;It is the decision to make i18next a &lt;strong&gt;translation engine behind an application-owned localization architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A robust system has three distinct layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Locale architecture
    routing, detection, user preference, fallback policy

Translation architecture
    schemas, catalogs, ownership, merging, plugins, types

Rendering integration
    i18next, React/Astro adapters, t(), formatting, plurals
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping those layers separate makes the system easier to test, extend, migrate between frameworks, and operate at scale.&lt;/p&gt;

&lt;p&gt;If only a few Starlight ideas are adopted, the highest-value ones are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;resolve locale outside i18next;&lt;/li&gt;
&lt;li&gt;treat translation catalogs as validated data;&lt;/li&gt;
&lt;li&gt;establish explicit source precedence;&lt;/li&gt;
&lt;li&gt;expose a small locale-bound translation API;&lt;/li&gt;
&lt;li&gt;type translation keys;&lt;/li&gt;
&lt;li&gt;give packages/plugins a registration mechanism;&lt;/li&gt;
&lt;li&gt;centralize third-party UI translations;&lt;/li&gt;
&lt;li&gt;design fallback and RTL behavior as application features rather than afterthoughts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those principles transfer directly from Starlight to React, Next.js, server applications, component libraries, monorepos, and other TypeScript projects.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>i18n</category>
      <category>startlight</category>
      <category>language</category>
    </item>
    <item>
      <title>Bare-Metal Convex: Develop and Deploy with the Minimum Stack Possible</title>
      <dc:creator>Fredy Sandoval</dc:creator>
      <pubDate>Wed, 12 Aug 2026 06:33:42 +0000</pubDate>
      <link>https://dev.to/fredysandoval/bare-metal-convex-develop-and-deploy-with-the-minimum-stack-possible-30co</link>
      <guid>https://dev.to/fredysandoval/bare-metal-convex-develop-and-deploy-with-the-minimum-stack-possible-30co</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A reusable runbook for self-hosting Convex directly from the precompiled binary, without Docker, Podman, Nginx, or Caddy.&lt;/p&gt;

&lt;p&gt;Tested workflow: Linux, SQLite, multiple independent Convex processes, Tailscale for administration, and Cloudflare Tunnel for public HTTPS.&lt;/p&gt;

&lt;p&gt;Last verified: August 2026.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What this setup is
&lt;/h2&gt;

&lt;p&gt;Convex can be self-hosted from a precompiled native binary. Docker is optional.&lt;/p&gt;

&lt;p&gt;For a small deployment, the stack can be reduced to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Development machine
├── convex-local-backend
├── Node.js + Convex CLI
└── frontend dev server

Production VM
├── convex-local-backend
├── tailscaled
├── cloudflared
└── systemd

No Docker
No Podman
No Nginx
No Caddy
No public SSH port
No public Convex ports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Convex backend uses SQLite by default and stores its database in the process's &lt;strong&gt;current working directory&lt;/strong&gt;, not beside the executable.&lt;/p&gt;

&lt;p&gt;The dashboard is optional. It is an administrative UI and does not need to run for the application to work.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Download the correct Convex backend binary
&lt;/h2&gt;

&lt;p&gt;Convex publishes precompiled &lt;code&gt;convex-local-backend&lt;/code&gt; binaries in GitHub Releases.&lt;/p&gt;

&lt;p&gt;For x86-64 Linux, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;convex-local-backend-x86_64-unknown-linux-gnu.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For ARM64 Linux, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;convex-local-backend-aarch64-unknown-linux-gnu.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Architecture matters. An x86-64 binary will not normally run on an ARM64 machine.&lt;/p&gt;

&lt;p&gt;Unzip it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unzip convex-local-backend-x86_64-unknown-linux-gnu.zip
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x convex-local-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ZIP contains one large executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;convex-local-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A convenient per-user installation location is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.local/bin
&lt;span class="nb"&gt;mv &lt;/span&gt;convex-local-backend ~/.local/bin/
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ~/.local/bin/convex-local-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;convex-local-backend &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The binary does not need to live beside your application data.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Directory model
&lt;/h2&gt;

&lt;p&gt;One executable can run many independent Convex instances.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.local/bin/
└── convex-local-backend

~/Documents/Convex/
├── app1/
├── app2/
└── app3/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each instance gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;its own process&lt;/li&gt;
&lt;li&gt;its own instance name&lt;/li&gt;
&lt;li&gt;its own instance secret&lt;/li&gt;
&lt;li&gt;its own admin key&lt;/li&gt;
&lt;li&gt;its own TCP ports&lt;/li&gt;
&lt;li&gt;its own SQLite database&lt;/li&gt;
&lt;li&gt;its own local storage directory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same executable can be used by all of them.&lt;/p&gt;

&lt;p&gt;This is the important model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;same convex-local-backend executable
        |
        ├── process app1
        │   └── database/storage app1
        |
        ├── process app2
        │   └── database/storage app2
        |
        └── process app3
            └── database/storage app3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; one Convex process hosting multiple databases. Each deployment is a separate process.&lt;/p&gt;




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

&lt;h2&gt;
  
  
  3. Create the first instance
&lt;/h2&gt;

&lt;p&gt;Create a directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/Documents/Convex/app1
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Documents/Convex/app1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate a random instance secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns a 64-character hexadecimal value.&lt;/p&gt;

&lt;p&gt;Do not derive production instance secrets from short human passwords. A generated random 32-byte secret is the safer default.&lt;/p&gt;

&lt;p&gt;The instance secret is extremely sensitive. Convex describes it as the root secret for the backend. Rotating it invalidates keys and sessions derived from it.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Create &lt;code&gt;start.sh&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;For a local development instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano start.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1

&lt;span class="nv"&gt;INSTANCE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"app1"&lt;/span&gt;
&lt;span class="nv"&gt;INSTANCE_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"PASTE_A_RANDOM_64_CHARACTER_SECRET_HERE"&lt;/span&gt;

&lt;span class="nb"&gt;exec &lt;/span&gt;convex-local-backend &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-secret&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_SECRET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--disable-beacon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Protect it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;700 start.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then start Convex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./start.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful first startup contains messages similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Connected to SQLite at convex_local_backend.sqlite3
...
backend listening on 0.0.0.0:3210
...
backend_http_proxy listening on 0.0.0.0:3211
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After startup, the directory will contain data similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app1/
├── start.sh
├── convex_local_backend.sqlite3
└── convex_local_storage/
    ├── files/
    ├── modules/
    ├── search/
    ├── exports/
    └── snapshot_imports/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This proves that the database/storage follows the &lt;strong&gt;working directory&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  About &lt;code&gt;--disable-beacon&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Self-hosted Convex includes a telemetry beacon that periodically contacts Convex.&lt;/p&gt;

&lt;p&gt;We observed it send a request to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.convex.dev/api/self_host_beacon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Convex documents the beacon as containing minimal deployment information such as a random deployment identifier, migration version, backend Git revision, and uptime.&lt;/p&gt;

&lt;p&gt;It is optional.&lt;/p&gt;

&lt;p&gt;To disable it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--disable-beacon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the flag enabled, the &lt;code&gt;local_backend::beacon&lt;/code&gt; startup/request lines disappear from the logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The &lt;code&gt;UDF fetch requests are unrestricted&lt;/code&gt; warning
&lt;/h2&gt;

&lt;p&gt;You may see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Running without a proxy in release mode -- UDF `fetch` requests are unrestricted!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; a telemetry warning.&lt;/p&gt;

&lt;p&gt;It means Convex functions that execute &lt;code&gt;fetch()&lt;/code&gt; can make outbound network requests without a Convex-controlled filtering proxy.&lt;/p&gt;

&lt;p&gt;For example:&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;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com/data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can make an outbound request from the Convex host.&lt;/p&gt;

&lt;p&gt;For local development this is usually acceptable.&lt;/p&gt;

&lt;p&gt;For production, remember that a compromised or badly written function may be able to reach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;public Internet services&lt;/li&gt;
&lt;li&gt;localhost services&lt;/li&gt;
&lt;li&gt;private network services reachable from the VM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cloudflare Tunnel protects &lt;strong&gt;inbound&lt;/strong&gt; access. It does not restrict outbound &lt;code&gt;fetch()&lt;/code&gt; calls from Convex functions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Admin keys
&lt;/h2&gt;

&lt;h2&gt;
  
  
  6. Generate the Convex admin key
&lt;/h2&gt;

&lt;p&gt;The downloaded backend release does not currently include a precompiled &lt;code&gt;generate_key&lt;/code&gt; helper.&lt;/p&gt;

&lt;p&gt;Convex's official direct-binary instructions use the Rust &lt;code&gt;keybroker&lt;/code&gt; utility from the source repository.&lt;/p&gt;

&lt;p&gt;Clone the backend source once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/get-convex/convex-backend.git
&lt;span class="nb"&gt;cd &lt;/span&gt;convex-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the &lt;strong&gt;same instance secret&lt;/strong&gt; used by the backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;INSTANCE_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"YOUR_APP1_INSTANCE_SECRET"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate the key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo run &lt;span class="nt"&gt;-p&lt;/span&gt; keybroker &lt;span class="nt"&gt;--bin&lt;/span&gt; generate_key &lt;span class="nt"&gt;--&lt;/span&gt; app1 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_SECRET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting admin key looks conceptually like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app1|...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the complete value in a password manager or another secure secret store.&lt;/p&gt;

&lt;p&gt;The admin key is what the Convex CLI and dashboard use to administer that backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build the helper once
&lt;/h3&gt;

&lt;p&gt;Cargo can produce a reusable binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; keybroker &lt;span class="nt"&gt;--bin&lt;/span&gt; generate_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result should be under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;target/release/generate_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;target/release/generate_key ~/.local/bin/convex-generate-key
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ~/.local/bin/convex-generate-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;convex-generate-key app1 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_SECRET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The helper binary is CPU/OS specific.&lt;/p&gt;

&lt;p&gt;An x86-64 Linux build should not be expected to run on ARM64 Linux. The generated &lt;strong&gt;admin key itself&lt;/strong&gt;, however, is just credential data and is not architecture-specific.&lt;/p&gt;

&lt;p&gt;You usually do not need the key generator on production servers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connect application code
&lt;/h2&gt;

&lt;h2&gt;
  
  
  7. Point a Convex project at the local backend
&lt;/h2&gt;

&lt;p&gt;Inside the actual frontend/application project:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.env.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;app1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONVEX_SELF_HOSTED_URL=http://127.0.0.1:3210
CONVEX_SELF_HOSTED_ADMIN_KEY=app1|PASTE_THE_COMPLETE_ADMIN_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not commit &lt;code&gt;.env.local&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx convex dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Successful output includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developing against deployment:
└─ http://127.0.0.1:3210

Convex functions ready!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI may also write frontend variables such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VITE_CONVEX_URL=http://127.0.0.1:3210
VITE_CONVEX_SITE_URL=http://127.0.0.1:3211
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is normal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Warnings we encountered
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Node localStorage experimental warning
&lt;/h4&gt;

&lt;p&gt;We saw:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ExperimentalWarning: localStorage is not available because --localstorage-file was not provided.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deployment still completed successfully. In our test this warning was not a Convex backend failure.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;/tmp&lt;/code&gt; and project are on different filesystems
&lt;/h4&gt;

&lt;p&gt;We saw:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Temporary directory '/tmp' and project directory ... are on different filesystems.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Convex says to use &lt;code&gt;CONVEX_TMPDIR&lt;/code&gt; if this causes problems with filesystem watchers.&lt;/p&gt;

&lt;p&gt;If everything works, it can be left alone.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;Filesystem changed during push, retrying...&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;We saw this during &lt;code&gt;npx convex dev&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Convex automatically retried and then reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Convex functions ready!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was successful behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Development process model
&lt;/h2&gt;

&lt;p&gt;During active development, keep these running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Terminal 1
./start.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Terminal 2
npx convex dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And, for a Vite app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Terminal 3
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npx convex dev&lt;/code&gt; is a watcher. It continuously watches the &lt;code&gt;convex/&lt;/code&gt; code and pushes changes into the deployment.&lt;/p&gt;

&lt;p&gt;It is development tooling, not the permanent production runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multiple independent apps on one machine
&lt;/h2&gt;

&lt;h2&gt;
  
  
  9. Create &lt;code&gt;app2&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Use a completely different secret and different ports.&lt;/p&gt;

&lt;p&gt;Example allocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app1
backend: 3210
site:    3211

app2
backend: 3220
site:    3221

app3
backend: 3230
site:    3231
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/Documents/Convex/app2
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Documents/Convex/app2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate a new random secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;start.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1

&lt;span class="nv"&gt;INSTANCE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"app2"&lt;/span&gt;
&lt;span class="nv"&gt;INSTANCE_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"PASTE_APP2_SECRET_HERE"&lt;/span&gt;

&lt;span class="nb"&gt;exec &lt;/span&gt;convex-local-backend &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-secret&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_SECRET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 3220 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--site-proxy-port&lt;/span&gt; 3221 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--convex-origin&lt;/span&gt; &lt;span class="s2"&gt;"http://127.0.0.1:3220"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--convex-site&lt;/span&gt; &lt;span class="s2"&gt;"http://127.0.0.1:3221"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--disable-beacon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;700 start.sh
./start.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;strong&gt;local development&lt;/strong&gt;, localhost origins are appropriate.&lt;/p&gt;

&lt;p&gt;Generate a separate admin key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;convex-generate-key app2 &lt;span class="s2"&gt;"THE_APP2_INSTANCE_SECRET"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point app2's project at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONVEX_SELF_HOSTED_URL=http://127.0.0.1:3220
CONVEX_SELF_HOSTED_ADMIN_KEY=app2|THE_APP2_ADMIN_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx convex dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developing against deployment:
└─ http://127.0.0.1:3220
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now app1 and app2 are isolated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app1 project
    ↓
127.0.0.1:3210
    ↓
app1 Convex process
    ↓
app1 SQLite/storage


app2 project
    ↓
127.0.0.1:3220
    ↓
app2 Convex process
    ↓
app2 SQLite/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stopping one process does not stop the other.&lt;/p&gt;

&lt;p&gt;Deploying code to one does not deploy it to the other.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Verify processes
&lt;/h2&gt;

&lt;p&gt;The simplest checks that worked in our test were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:3210/version&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo
&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:3220/version&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our precompiled build returned:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The important result was that both ports responded.&lt;/p&gt;

&lt;p&gt;We also verified separate processes with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps aux | &lt;span class="nb"&gt;grep &lt;/span&gt;convex-local-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That showed two different PIDs and different instance/port arguments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security lesson: &lt;code&gt;ps&lt;/code&gt; can expose the instance secret
&lt;/h3&gt;

&lt;p&gt;Because the official direct-binary invocation passes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--instance-secret &amp;lt;secret&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;as a command-line argument, a process listing can expose that secret.&lt;/p&gt;

&lt;p&gt;We observed exactly this with &lt;code&gt;ps aux&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Therefore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;never paste production process listings without redacting secrets&lt;/li&gt;
&lt;li&gt;use a dedicated Unix user for the Convex service in production&lt;/li&gt;
&lt;li&gt;restrict server access&lt;/li&gt;
&lt;li&gt;treat a secret shown in logs/chat/process listings as compromised&lt;/li&gt;
&lt;li&gt;regenerate exposed secrets before real production use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a production host with multiple local users, consider additional OS process-visibility hardening.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dashboard
&lt;/h2&gt;

&lt;h2&gt;
  
  
  11. The dashboard is optional
&lt;/h2&gt;

&lt;p&gt;The dashboard is not the Convex server.&lt;/p&gt;

&lt;p&gt;The application works with only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;frontend
   ↓
convex-local-backend
   ↓
SQLite/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dashboard is an administrative client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dashboard
   ↓
convex-local-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It can be stopped without stopping your application.&lt;/p&gt;

&lt;p&gt;The same dashboard build can be used to administer different deployments by using the appropriate backend URL and matching admin key.&lt;/p&gt;

&lt;p&gt;For a production environment, keep the dashboard private where possible, for example reachable only through Tailscale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production deployment
&lt;/h2&gt;

&lt;h2&gt;
  
  
  12. Replace &lt;code&gt;convex dev&lt;/code&gt; with a one-time deploy
&lt;/h2&gt;

&lt;p&gt;Development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx convex dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx convex deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We tested a successful self-hosted deployment and saw:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deploying code to deployment:
└─ http://127.0.0.1:3210

...
Schema validation complete.
Finalizing push...

Deployed Convex functions to http://127.0.0.1:3210
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;npx convex deploy&lt;/code&gt; finishes, there is no permanent CLI process.&lt;/p&gt;

&lt;p&gt;The production runtime is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;convex-local-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI is only used again when deploying changed Convex functions.&lt;/p&gt;

&lt;p&gt;Convex's documentation explicitly notes that the backend itself does not distinguish "development" from "production"; &lt;code&gt;convex dev&lt;/code&gt; continuously deploys while &lt;code&gt;convex deploy&lt;/code&gt; deploys once.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Build the frontend separately
&lt;/h2&gt;

&lt;p&gt;For Vite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This normally produces:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;For a local production-like test you can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx vite preview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;vite preview&lt;/code&gt; is a preview server, not the preferred permanent production web server.&lt;/p&gt;

&lt;p&gt;In this architecture, the cleanest production option is to host the built frontend separately, for example on a static hosting platform, and keep the VM focused on Convex.&lt;/p&gt;

&lt;p&gt;That eliminates the frontend Node process from the VM.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production networking: Tailscale + Cloudflare Tunnel
&lt;/h2&gt;

&lt;h2&gt;
  
  
  14. Why no Nginx or Caddy is required
&lt;/h2&gt;

&lt;p&gt;Cloudflare Tunnel can map public hostnames directly to local HTTP services.&lt;/p&gt;

&lt;p&gt;For Convex, that means &lt;code&gt;cloudflared&lt;/code&gt; can talk directly to the backend ports.&lt;/p&gt;

&lt;p&gt;The public path becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user
  ↓ HTTPS
Cloudflare
  ↓ encrypted Cloudflare Tunnel
cloudflared
  ↓ localhost HTTP
Convex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloudflare handles public HTTPS.&lt;/p&gt;

&lt;p&gt;The VM does not need a public port 80 or 443.&lt;/p&gt;

&lt;p&gt;Cloudflare Tunnel uses outbound connections initiated by &lt;code&gt;cloudflared&lt;/code&gt;, so the origin can have public inbound traffic blocked.&lt;/p&gt;

&lt;p&gt;Cloudflare Tunnel also supports WebSockets, which is important for Convex realtime clients.&lt;/p&gt;




&lt;h2&gt;
  
  
  15. Use two public Convex hostnames
&lt;/h2&gt;

&lt;p&gt;A direct Convex backend normally exposes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;backend/client API: 3210
HTTP actions/site:  3211
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production, use public HTTPS origins such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://convex-api.example.com
https://convex-site.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloudflare Tunnel routes them to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;convex-api.example.com
    → http://127.0.0.1:3210

convex-site.example.com
    → http://127.0.0.1:3211
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the &lt;strong&gt;production&lt;/strong&gt; Convex process should know its public origins.&lt;/p&gt;

&lt;p&gt;Example production &lt;code&gt;start.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1

&lt;span class="nv"&gt;INSTANCE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"app1"&lt;/span&gt;
&lt;span class="nv"&gt;INSTANCE_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ./instance-secret&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;exec &lt;/span&gt;convex-local-backend &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-secret&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_SECRET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 3210 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--site-proxy-port&lt;/span&gt; 3211 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--convex-origin&lt;/span&gt; &lt;span class="s2"&gt;"https://convex-api.example.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--convex-site&lt;/span&gt; &lt;span class="s2"&gt;"https://convex-site.example.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--disable-beacon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store the secret separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; instance-secret
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 instance-secret
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 start.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not regenerate &lt;code&gt;instance-secret&lt;/code&gt; on each boot.&lt;/p&gt;

&lt;p&gt;The public origins matter because Convex may generate URLs that refer back to itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  16. Cloudflare Tunnel routing
&lt;/h2&gt;

&lt;p&gt;With a locally-managed tunnel, the conceptual ingress configuration is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;convex-api.example.com&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3210&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;convex-site.example.com&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3211&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http_status:404&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloudflare provides the public HTTPS certificates.&lt;/p&gt;

&lt;p&gt;The local hop can remain plain HTTP because:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared → 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;stays on the VM.&lt;/p&gt;

&lt;p&gt;If your Cloudflare account has a WebSockets setting, make sure WebSockets are enabled. Cloudflare documents full WebSocket support for Tunnel.&lt;/p&gt;

&lt;p&gt;For a second Convex app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app1-api.example.com&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3210&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app1-site.example.com&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3211&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app2-api.example.com&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3220&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app2-site.example.com&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3221&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http_status:404&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No local reverse proxy is required just to perform hostname routing.&lt;/p&gt;

&lt;p&gt;Add Caddy/Nginx only if you later need a feature Cloudflare Tunnel is not providing, such as complicated local rewrite logic or local load balancing.&lt;/p&gt;




&lt;h2&gt;
  
  
  17. Production application environment
&lt;/h2&gt;

&lt;p&gt;When deploying from your development/CI machine to the production backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONVEX_SELF_HOSTED_URL=https://convex-api.example.com
CONVEX_SELF_HOSTED_ADMIN_KEY=app1|PRODUCTION_ADMIN_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx convex deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://127.0.0.1:3210
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;from a remote development machine; that would refer to the development machine itself.&lt;/p&gt;

&lt;p&gt;The frontend production build should also receive the public Convex URLs appropriate to its framework, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VITE_CONVEX_URL=https://convex-api.example.com
VITE_CONVEX_SITE_URL=https://convex-site.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact frontend variable names depend on the framework and what the Convex CLI/project generated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tailscale and firewall
&lt;/h2&gt;

&lt;h2&gt;
  
  
  18. Use Tailscale for administration
&lt;/h2&gt;

&lt;p&gt;Tailscale lets you keep SSH and administrative services off the public Internet.&lt;/p&gt;

&lt;p&gt;Typical administration path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your laptop
   ↓ Tailscale
production VM
   ├── SSH
   ├── logs
   └── private dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tailscale's current documentation says most installations do not require manually opening an inbound firewall port. When direct peer-to-peer connectivity is not possible it can fall back to relays.&lt;/p&gt;

&lt;p&gt;Before blocking public SSH, verify that you can log in using the VM's Tailscale IP.&lt;/p&gt;

&lt;p&gt;Get it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailscale ip &lt;span class="nt"&gt;-4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh user@100.x.y.z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only after that should you remove public SSH access.&lt;/p&gt;




&lt;h2&gt;
  
  
  19. UFW model for the VM
&lt;/h2&gt;

&lt;p&gt;A minimal model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default deny incoming
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default allow outgoing
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow &lt;span class="k"&gt;in &lt;/span&gt;on tailscale0
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw status verbose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Cloudflare Tunnel, you do not need public UFW rules for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;22
80
443
3210
3211
3220
3221
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;assuming administration is through Tailscale and all public app traffic comes through &lt;code&gt;cloudflared&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Cloudflare documents that Tunnel can work with inbound traffic blocked because &lt;code&gt;cloudflared&lt;/code&gt; creates outbound-only connections.&lt;/p&gt;

&lt;p&gt;Do not lock yourself out: verify Tailscale SSH/network access before deleting an existing public SSH rule.&lt;/p&gt;




&lt;h2&gt;
  
  
  systemd production service
&lt;/h2&gt;

&lt;h2&gt;
  
  
  20. Keep Convex running without a terminal
&lt;/h2&gt;

&lt;p&gt;Create a service unit such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/systemd/system/convex-app1.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Convex app1 backend&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;convex&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/srv/convex/app1&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/srv/convex/app1/start.sh&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application directory could be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/srv/convex/app1/
├── start.sh
├── instance-secret
├── convex_local_backend.sqlite3
└── convex_local_storage/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; convex-app1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status convex-app1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; convex-app1 &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;app2&lt;/code&gt;, create another unit using its own directory and ports.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resource usage findings
&lt;/h2&gt;

&lt;h2&gt;
  
  
  21. What we observed
&lt;/h2&gt;

&lt;p&gt;In our small idle test:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Approx. RAM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;convex-tutorial&lt;/code&gt; production-like process set&lt;/td&gt;
&lt;td&gt;~276 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;presence-typing-indicator&lt;/code&gt; production-like process set&lt;/td&gt;
&lt;td&gt;~348 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Combined&lt;/td&gt;
&lt;td&gt;~624 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Idle CPU was negligible.&lt;/p&gt;

&lt;p&gt;With duplicate development stacks using &lt;code&gt;convex dev&lt;/code&gt;, measured combined usage had previously been roughly 1.5 GB.&lt;/p&gt;

&lt;p&gt;Moving to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;convex-local-backend
+
one-time convex deploy
+
built frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;reduced the observed idle footprint substantially.&lt;/p&gt;

&lt;p&gt;These are &lt;strong&gt;environment-specific measurements&lt;/strong&gt;, not guaranteed Convex requirements.&lt;/p&gt;

&lt;p&gt;For real capacity planning, measure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VM baseline before apps&lt;/li&gt;
&lt;li&gt;incremental memory after each Convex instance&lt;/li&gt;
&lt;li&gt;real traffic&lt;/li&gt;
&lt;li&gt;search/vector workloads&lt;/li&gt;
&lt;li&gt;file storage&lt;/li&gt;
&lt;li&gt;scheduled jobs&lt;/li&gt;
&lt;li&gt;OS/cache usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PSS is preferable to blindly summing RSS when possible.&lt;/p&gt;

&lt;p&gt;For a 2 GB VM, a conservative starting target is usually fewer applications than the mathematical maximum. Leave headroom for Linux, Tailscale, &lt;code&gt;cloudflared&lt;/code&gt;, filesystem cache, traffic bursts, and maintenance operations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Important production notes
&lt;/h2&gt;

&lt;h2&gt;
  
  
  22. SQLite is the minimum, not necessarily the final database architecture
&lt;/h2&gt;

&lt;p&gt;Convex defaults to SQLite for self-hosting and recommends starting with the basic configuration.&lt;/p&gt;

&lt;p&gt;For a mini production environment this can be useful because it eliminates a separate database server.&lt;/p&gt;

&lt;p&gt;However, production requirements eventually determine whether you should move to Postgres/MySQL and/or external object storage.&lt;/p&gt;

&lt;p&gt;At minimum, back up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;convex_local_backend.sqlite3
convex_local_storage/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and test restoration.&lt;/p&gt;

&lt;p&gt;Do not assume a copied live SQLite file is a valid backup without using an appropriate consistent-backup procedure.&lt;/p&gt;




&lt;h2&gt;
  
  
  23. Do not confuse three different kinds of environment variables
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Backend process configuration
&lt;/h3&gt;

&lt;p&gt;Used to configure the &lt;code&gt;convex-local-backend&lt;/code&gt; process itself.&lt;/p&gt;

&lt;p&gt;In this runbook we mostly use explicit flags in &lt;code&gt;start.sh&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Convex CLI self-hosted connection
&lt;/h3&gt;

&lt;p&gt;Used by your source project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONVEX_SELF_HOSTED_URL=...
CONVEX_SELF_HOSTED_ADMIN_KEY=...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These tell &lt;code&gt;npx convex dev&lt;/code&gt; and &lt;code&gt;npx convex deploy&lt;/code&gt; which self-hosted backend to administer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Convex function environment variables
&lt;/h3&gt;

&lt;p&gt;Application secrets such as third-party API keys belong inside the Convex deployment environment and are accessed by Convex functions.&lt;/p&gt;

&lt;p&gt;These are conceptually separate from the instance secret and admin key.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common mistakes from this test
&lt;/h2&gt;

&lt;h2&gt;
  
  
  24. Mistakes to avoid
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake: thinking the dashboard is required
&lt;/h3&gt;

&lt;p&gt;It is not. It is an admin UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake: leaving &lt;code&gt;npx convex dev&lt;/code&gt; running in production
&lt;/h3&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx convex deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for a one-time deployment.&lt;/p&gt;

&lt;p&gt;Keep only &lt;code&gt;convex-local-backend&lt;/code&gt; running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake: using one secret for multiple apps
&lt;/h3&gt;

&lt;p&gt;Each independent backend should have its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;instance secret&lt;/li&gt;
&lt;li&gt;admin key&lt;/li&gt;
&lt;li&gt;ports&lt;/li&gt;
&lt;li&gt;database/storage directory&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Mistake: running two instances on the same ports
&lt;/h3&gt;

&lt;p&gt;Allocate separate pairs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3210/3211
3220/3221
3230/3231
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mistake: regenerating &lt;code&gt;INSTANCE_SECRET&lt;/code&gt; every boot
&lt;/h3&gt;

&lt;p&gt;Do not do this.&lt;/p&gt;

&lt;p&gt;The same deployment must keep the same instance secret unless you intentionally rotate it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake: exposing secrets with &lt;code&gt;ps&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Direct CLI arguments can be visible in process listings.&lt;/p&gt;

&lt;p&gt;Do not publish unredacted &lt;code&gt;ps&lt;/code&gt; output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake: assuming &lt;code&gt;/version&lt;/code&gt; must display a release number
&lt;/h3&gt;

&lt;p&gt;Our precompiled backend returned:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;while functioning correctly.&lt;/p&gt;

&lt;p&gt;Use it mainly as a reachability/health signal unless your build provides a meaningful version string.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake: thinking Cloudflare Tunnel restricts Convex &lt;code&gt;fetch()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;It does not.&lt;/p&gt;

&lt;p&gt;Tunnel handles inbound/public access. Convex UDF &lt;code&gt;fetch()&lt;/code&gt; is outbound traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake: keeping localhost origins in production
&lt;/h3&gt;

&lt;p&gt;Local development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://127.0.0.1:3210
http://127.0.0.1:3211
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production behind Cloudflare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://convex-api.example.com
https://convex-site.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The production backend should know its public origins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake: assuming Vite preview is the final production host
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;vite preview&lt;/code&gt; is useful to test a production build.&lt;/p&gt;

&lt;p&gt;For a real deployment, host the static build on a proper static host. If the frontend is hosted away from the Convex VM, the VM uses less RAM.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final architecture
&lt;/h2&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer PC

app source
   |
   ├── npm run dev
   │      ↓
   │   Vite frontend
   │
   └── npx convex dev
          ↓
     localhost:3210
          ↓
convex-local-backend
          ↓
SQLite + local storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Production
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         ┌─────────────────────┐
                         │   Static frontend   │
                         │  (separate hosting) │
                         └──────────┬──────────┘
                                    |
                                    | HTTPS
                                    v
                                 Browser
                                    |
                                    | HTTPS / WebSocket
                                    v
                              Cloudflare edge
                                    |
                             Cloudflare Tunnel
                                    |
                                    v
                            production VM
                     ┌──────────────┼──────────────┐
                     │              │              │
                 tailscaled     cloudflared    systemd
                     │              │              │
                 private admin      │        convex-local-backend
                                    │              │
                                    ├── :3210 API  │
                                    └── :3211 site │
                                                   │
                                              SQLite/storage

Public inbound firewall:
DENY

Administrative access:
Tailscale

Public application access:
Cloudflare Tunnel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the minimum production stack we were aiming for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Convex binary
+ SQLite/local storage
+ systemd
+ Tailscale
+ Cloudflare Tunnel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No container runtime and no local reverse proxy are inherently required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick rebuild checklist
&lt;/h2&gt;

&lt;p&gt;If rebuilding from zero:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download the correct &lt;code&gt;convex-local-backend&lt;/code&gt; architecture.&lt;/li&gt;
&lt;li&gt;Put it in &lt;code&gt;~/.local/bin&lt;/code&gt; or a system binary directory.&lt;/li&gt;
&lt;li&gt;Create a dedicated directory for each instance.&lt;/li&gt;
&lt;li&gt;Generate a random 32-byte instance secret.&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;start.sh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Assign unique ports for each instance.&lt;/li&gt;
&lt;li&gt;Start the backend and confirm SQLite/storage paths.&lt;/li&gt;
&lt;li&gt;Build &lt;code&gt;convex-generate-key&lt;/code&gt; once from the Convex source repository.&lt;/li&gt;
&lt;li&gt;Generate a unique admin key for each backend.&lt;/li&gt;
&lt;li&gt;Put &lt;code&gt;CONVEX_SELF_HOSTED_URL&lt;/code&gt; and &lt;code&gt;CONVEX_SELF_HOSTED_ADMIN_KEY&lt;/code&gt; in the application project's uncommitted environment file.&lt;/li&gt;
&lt;li&gt;Develop with &lt;code&gt;npx convex dev&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Release backend functions with &lt;code&gt;npx convex deploy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Build the frontend separately.&lt;/li&gt;
&lt;li&gt;On production, set Convex's public origins to the Cloudflare HTTPS hostnames.&lt;/li&gt;
&lt;li&gt;Route the API/site hostnames through Cloudflare Tunnel to the local Convex ports.&lt;/li&gt;
&lt;li&gt;Verify Cloudflare WebSockets are enabled.&lt;/li&gt;
&lt;li&gt;Install Tailscale and verify remote access through the tailnet.&lt;/li&gt;
&lt;li&gt;Deny public inbound traffic and allow the &lt;code&gt;tailscale0&lt;/code&gt; interface.&lt;/li&gt;
&lt;li&gt;Run Convex under systemd.&lt;/li&gt;
&lt;li&gt;Back up the SQLite database and local storage, and test restores.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;This runbook combines direct testing with current official documentation. Re-check these sources before a future major upgrade because self-hosted Convex is actively developed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Convex self-hosting README:
&lt;a href="https://github.com/get-convex/convex-backend/blob/main/self-hosted/README.md" rel="noopener noreferrer"&gt;https://github.com/get-convex/convex-backend/blob/main/self-hosted/README.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Convex: running the precompiled binary directly:
&lt;a href="https://github.com/get-convex/convex-backend/blob/main/self-hosted/advanced/running_binary_directly.md" rel="noopener noreferrer"&gt;https://github.com/get-convex/convex-backend/blob/main/self-hosted/advanced/running_binary_directly.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Convex self-hosted Fly guide, including &lt;code&gt;convex dev&lt;/code&gt; vs &lt;code&gt;convex deploy&lt;/code&gt; and public origins:
&lt;a href="https://github.com/get-convex/convex-backend/blob/main/self-hosted/advanced/fly/README.md" rel="noopener noreferrer"&gt;https://github.com/get-convex/convex-backend/blob/main/self-hosted/advanced/fly/README.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Convex backend repository/self-hosting notes and beacon description:
&lt;a href="https://github.com/get-convex/convex-backend" rel="noopener noreferrer"&gt;https://github.com/get-convex/convex-backend&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cloudflare Tunnel overview:
&lt;a href="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/" rel="noopener noreferrer"&gt;https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cloudflare published applications:
&lt;a href="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/" rel="noopener noreferrer"&gt;https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cloudflare Tunnel WebSocket support:
&lt;a href="https://developers.cloudflare.com/cloudflare-one/faq/cloudflare-tunnels-faq/" rel="noopener noreferrer"&gt;https://developers.cloudflare.com/cloudflare-one/faq/cloudflare-tunnels-faq/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tailscale firewall-port guidance:
&lt;a href="https://tailscale.com/docs/reference/faq/firewall-ports" rel="noopener noreferrer"&gt;https://tailscale.com/docs/reference/faq/firewall-ports&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tailscale UFW server-lockdown guide:
&lt;a href="https://tailscale.com/docs/how-to/secure-ubuntu-server-with-ufw" rel="noopener noreferrer"&gt;https://tailscale.com/docs/how-to/secure-ubuntu-server-with-ufw&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>convex</category>
    </item>
    <item>
      <title>How to Compile and Run Convex Locally from Source</title>
      <dc:creator>Fredy Sandoval</dc:creator>
      <pubDate>Wed, 12 Aug 2026 03:41:35 +0000</pubDate>
      <link>https://dev.to/fredysandoval/how-to-compile-and-run-convex-locally-from-source-3bki</link>
      <guid>https://dev.to/fredysandoval/how-to-compile-and-run-convex-locally-from-source-3bki</guid>
      <description>&lt;p&gt;This guide explains how to build and run the Convex backend from source on&lt;br&gt;
Arch Linux. It is written for developers who are new to the repository and&lt;br&gt;
want a reliable local development setup.&lt;/p&gt;

&lt;p&gt;The commands below assume that you are working from the root of the cloned&lt;br&gt;
repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Documents/OpenSource/convex-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What is in this repository?
&lt;/h2&gt;

&lt;p&gt;Convex is an open-source reactive backend and database platform. Applications&lt;br&gt;
define database functions in TypeScript, while the backend provides database&lt;br&gt;
storage, queries, mutations, actions, file storage, scheduled jobs,&lt;br&gt;
authentication, synchronization, search, and related services.&lt;/p&gt;

&lt;p&gt;The repository has two main areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;crates/&lt;/code&gt; contains the Rust backend workspace. The &lt;code&gt;local_backend&lt;/code&gt; crate
produces the &lt;code&gt;convex-local-backend&lt;/code&gt; executable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm-packages/&lt;/code&gt; contains the JavaScript and TypeScript monorepo, managed by
pnpm and Turborepo. It includes the Convex client, CLI, dashboards, runtime
packages, tests, and demos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a top-level &lt;code&gt;demo/&lt;/code&gt; directory containing a separate Vite/React&lt;br&gt;
example, plus &lt;code&gt;self-hosted/&lt;/code&gt; documentation for deployment and prebuilt&lt;br&gt;
binary usage.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Install Arch Linux system dependencies
&lt;/h2&gt;

&lt;p&gt;Install the compiler toolchain and native libraries required by Rust and the&lt;br&gt;
JavaScript tooling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-Syu&lt;/span&gt; &lt;span class="nt"&gt;--needed&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  base-devel &lt;span class="se"&gt;\&lt;/span&gt;
  git &lt;span class="se"&gt;\&lt;/span&gt;
  curl &lt;span class="se"&gt;\&lt;/span&gt;
  openssl &lt;span class="se"&gt;\&lt;/span&gt;
  libsodium &lt;span class="se"&gt;\&lt;/span&gt;
  rocksdb &lt;span class="se"&gt;\&lt;/span&gt;
  snappy &lt;span class="se"&gt;\&lt;/span&gt;
  pkgconf &lt;span class="se"&gt;\&lt;/span&gt;
  python &lt;span class="se"&gt;\&lt;/span&gt;
  rustup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;base-devel&lt;/code&gt; supplies common build tools such as GCC, Make, and Binutils.&lt;/p&gt;

&lt;p&gt;You should also have &lt;code&gt;fnm&lt;/code&gt; installed. The repository does not require NVM;&lt;br&gt;
&lt;code&gt;fnm&lt;/code&gt; is sufficient for managing Node.js versions. On Arch Linux it can be&lt;br&gt;
installed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="nt"&gt;--needed&lt;/span&gt; fnm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Configure fnm and install the required Node.js version
&lt;/h2&gt;

&lt;p&gt;Convex pins Node.js to version &lt;code&gt;22.22.2&lt;/code&gt; in &lt;code&gt;.nvmrc&lt;/code&gt;. Configure fnm for the&lt;br&gt;
current Bash session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;fnm &lt;span class="nb"&gt;env&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; bash&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
fnm &lt;span class="nb"&gt;install &lt;/span&gt;22.22.2 &lt;span class="nt"&gt;--use&lt;/span&gt;
fnm default 22.22.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that the correct version is active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--version&lt;/span&gt;
npm &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected Node.js version is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v22.22.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To configure fnm automatically for future Bash sessions, add this line to&lt;br&gt;
&lt;code&gt;~/.bashrc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;fnm &lt;span class="nb"&gt;env&lt;/span&gt; &lt;span class="nt"&gt;--use-on-cd&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; bash&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reload Bash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use Zsh instead, add the equivalent line to &lt;code&gt;~/.zshrc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;fnm &lt;span class="nb"&gt;env&lt;/span&gt; &lt;span class="nt"&gt;--use-on-cd&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; zsh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Install the repository-pinned development tools
&lt;/h2&gt;

&lt;p&gt;Convex uses &lt;code&gt;mise&lt;/code&gt; to install and manage several exact tool versions,&lt;br&gt;
including Just, CMake, Protocol Buffers, jq, uv, and Rust cargo utilities.&lt;/p&gt;

&lt;p&gt;Install mise using its official installer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://mise.run | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mise will give you instructions on how to add it to your shell for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mise: installed successfully to /home/YourUser/.local/bin/mise
mise: run the following to activate mise &lt;span class="k"&gt;in &lt;/span&gt;your shell:
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"eval &lt;/span&gt;&lt;span class="se"&gt;\"\$&lt;/span&gt;&lt;span class="s2"&gt;(/home/YourUser/.local/bin/mise activate zsh)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"/home/YourUser/.zshrc"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Install the versions declared by &lt;code&gt;mise.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The repository currently pins these important tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;&lt;code&gt;22.22.2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nightly-2026-06-28&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pnpm&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.34.5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turborepo&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2.10.5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Just&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.52.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CMake&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3.31.12&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;protoc&lt;/td&gt;
&lt;td&gt;&lt;code&gt;21.12&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uv&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.11.5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jq&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.8.1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rust automatically uses the toolchain selected by &lt;code&gt;rust-toolchain&lt;/code&gt; when you&lt;br&gt;
run commands inside this repository. Verify it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rustc &lt;span class="nt"&gt;--version&lt;/span&gt;
cargo &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Rust reports that the toolchain is not installed, install it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rustup toolchain &lt;span class="nb"&gt;install &lt;/span&gt;nightly-2026-06-28
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Install the JavaScript tooling
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;scripts/&lt;/code&gt; package contains the repository's pinned versions of pnpm,&lt;br&gt;
Turborepo, dprint, and related scripts. Install them with npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm clean-install &lt;span class="nt"&gt;--prefix&lt;/span&gt; scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then install the locked JavaScript workspace dependencies:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The lockfile is &lt;code&gt;npm-packages/pnpm-lock.yaml&lt;/code&gt;. Keep the lockfile intact and&lt;br&gt;
prefer the repository commands over installing random global versions of&lt;br&gt;
pnpm or Turborepo.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Compile the backend from source
&lt;/h2&gt;

&lt;p&gt;The local backend is the Rust binary named &lt;code&gt;convex-local-backend&lt;/code&gt;. To compile&lt;br&gt;
it directly in debug mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo build &lt;span class="nt"&gt;--locked&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; local_backend &lt;span class="nt"&gt;--bin&lt;/span&gt; convex-local-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiled binary will be under Cargo's &lt;code&gt;target/&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;For a release build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;--locked&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; local_backend &lt;span class="nt"&gt;--bin&lt;/span&gt; convex-local-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first build can take a long time. Some dependencies include V8 and&lt;br&gt;
Chromium-related source repositories, so Cargo may need to download and&lt;br&gt;
prepare a large dependency graph before Rust compilation begins.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Run the backend locally
&lt;/h2&gt;

&lt;p&gt;The recommended development command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just run-local-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts the backend from source and creates the local instance credentials&lt;br&gt;
when necessary.&lt;/p&gt;

&lt;p&gt;The default local endpoints are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Address&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Convex backend&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://127.0.0.1:3210&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP Actions proxy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://127.0.0.1:3211&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Local state is stored in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;convex_local_backend.sqlite3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;convex_local_storage/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To pass an option to the backend, place it after the Just command, for&lt;br&gt;
example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just run-local-backend &lt;span class="nt"&gt;--disable-beacon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To remove local backend state and start over:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just reset-local-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the backend terminal running while using the CLI or a demo. Stop it with&lt;br&gt;
&lt;code&gt;Ctrl+C&lt;/code&gt; when finished.&lt;/p&gt;
&lt;h2&gt;
  
  
  7. Build the JavaScript and TypeScript monorepo
&lt;/h2&gt;

&lt;p&gt;Build all workspace packages and their dependencies with Turborepo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just turbo run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To build one package and its dependencies, use a filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just turbo run build &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;convex...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;...&lt;/code&gt; means “also build the dependencies of this package.”&lt;/p&gt;

&lt;p&gt;For package-level development, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;npm-packages/convex
npm run build
npm run lint
npm run format-check
npm run typecheck
npm &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Return to the repository root when using the root-level Just commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../..
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Run the included tutorial demo against the local backend
&lt;/h2&gt;

&lt;p&gt;Use two terminals.&lt;/p&gt;

&lt;p&gt;In terminal 1, start the backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Documents/OpenSource/convex-backend
just run-local-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In terminal 2, install the JavaScript workspace if you have not already done&lt;br&gt;
so, then start the tutorial's Convex development process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Documents/OpenSource/convex-backend
just install-js
&lt;span class="nb"&gt;cd &lt;/span&gt;npm-packages/demos/tutorial
just convex dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository's &lt;code&gt;just convex&lt;/code&gt; helper configures the local backend URL and&lt;br&gt;
uses the generated local admin key.&lt;/p&gt;

&lt;p&gt;There is also a separate top-level Vite/React demo. It is not part of the&lt;br&gt;
pnpm workspace, so install its dependencies separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Documents/OpenSource/convex-backend/demo
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. Useful development commands
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Rust formatting, tests, and linting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;fmt
&lt;/span&gt;cargo &lt;span class="nb"&gt;test
&lt;/span&gt;cargo &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; local_backend
cargo clippy
cargo nextest run &lt;span class="nt"&gt;--no-run&lt;/span&gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  JavaScript formatting, tests, and linting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just turbo run build
&lt;span class="nb"&gt;cd &lt;/span&gt;npm-packages/convex
npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm run lint
npm run format-check
npm run typecheck
npm run test-esm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Local Convex CLI helpers
&lt;/h3&gt;

&lt;p&gt;From the repository root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just convex dev
just convex data
just convex &lt;span class="nb"&gt;env
&lt;/span&gt;just convex logs
just convex import
just convex &lt;span class="nb"&gt;export&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  10. Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;node --version&lt;/code&gt; shows the wrong version
&lt;/h3&gt;

&lt;p&gt;Reload fnm and select the pinned version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;fnm &lt;span class="nb"&gt;env&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; bash&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
fnm &lt;span class="nb"&gt;install &lt;/span&gt;22.22.2 &lt;span class="nt"&gt;--use&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;just: command not found&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Ensure mise is on &lt;code&gt;PATH&lt;/code&gt;, then install the repository tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.local/bin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
mise &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cargo cannot download dependencies
&lt;/h3&gt;

&lt;p&gt;The backend has large Git-based dependencies. Confirm that Git and network&lt;br&gt;
access work, then retry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;--version&lt;/span&gt;
cargo build &lt;span class="nt"&gt;--locked&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; local_backend &lt;span class="nt"&gt;--bin&lt;/span&gt; convex-local-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  JavaScript installation fails with a DNS or registry error
&lt;/h3&gt;

&lt;p&gt;Retry after confirming npm connectivity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://registry.npmjs.org/
npm clean-install &lt;span class="nt"&gt;--prefix&lt;/span&gt; scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The demo cannot connect to Convex
&lt;/h3&gt;

&lt;p&gt;Make sure &lt;code&gt;just run-local-backend&lt;/code&gt; is still running in another terminal and&lt;br&gt;
that the backend is listening on port &lt;code&gt;3210&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://127.0.0.1:3210
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Start with a completely fresh local backend
&lt;/h3&gt;

&lt;p&gt;This removes only the repository's local backend state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just reset-local-backend
just run-local-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick-start summary
&lt;/h2&gt;

&lt;p&gt;Once Arch Linux prerequisites are installed, the normal workflow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Documents/OpenSource/convex-backend
&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;fnm &lt;span class="nb"&gt;env&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; bash&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
fnm use 22.22.2 &lt;span class="nt"&gt;--install-if-missing&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.local/bin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
mise &lt;span class="nb"&gt;install
&lt;/span&gt;npm clean-install &lt;span class="nt"&gt;--prefix&lt;/span&gt; scripts
just install-js
just run-local-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use a second terminal for the tutorial demo or JavaScript package&lt;br&gt;
development.&lt;/p&gt;

</description>
      <category>convex</category>
      <category>compiling</category>
    </item>
    <item>
      <title>Did you know you don't need a Raspberry PI to build a NAS, you can make a virtual nas instead.</title>
      <dc:creator>Fredy Sandoval</dc:creator>
      <pubDate>Thu, 06 Aug 2026 05:53:56 +0000</pubDate>
      <link>https://dev.to/fredysandoval/did-you-know-you-dont-need-a-raspberry-pi-to-build-a-nas-you-can-make-a-virtual-nas-instead-3ko9</link>
      <guid>https://dev.to/fredysandoval/did-you-know-you-dont-need-a-raspberry-pi-to-build-a-nas-you-can-make-a-virtual-nas-instead-3ko9</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/fredysandoval/making-a-virtual-nas-with-virt-manager-openmediavault-and-tailscale-22g8" class="crayons-story__hidden-navigation-link"&gt;Making a Virtual NAS with virt-manager, OpenMediaVault, and Tailscale&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/fredysandoval" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F889622%2Fef5b2f52-9cc2-4663-873d-523c6862df45.png" alt="fredysandoval profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/fredysandoval" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Fredy Sandoval
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Fredy Sandoval
                
              
              &lt;div id="story-author-preview-content-4328699" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/fredysandoval" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F889622%2Fef5b2f52-9cc2-4663-873d-523c6862df45.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Fredy Sandoval&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/fredysandoval/making-a-virtual-nas-with-virt-manager-openmediavault-and-tailscale-22g8" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Aug 6&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/fredysandoval/making-a-virtual-nas-with-virt-manager-openmediavault-and-tailscale-22g8" id="article-link-4328699"&gt;
          Making a Virtual NAS with virt-manager, OpenMediaVault, and Tailscale
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/nas"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;nas&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/virtmanager"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;virtmanager&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/openmediavault"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;openmediavault&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tailscale"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tailscale&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/fredysandoval/making-a-virtual-nas-with-virt-manager-openmediavault-and-tailscale-22g8#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>devops</category>
      <category>linux</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Making a Virtual NAS with virt-manager, OpenMediaVault, and Tailscale</title>
      <dc:creator>Fredy Sandoval</dc:creator>
      <pubDate>Thu, 06 Aug 2026 05:52:41 +0000</pubDate>
      <link>https://dev.to/fredysandoval/making-a-virtual-nas-with-virt-manager-openmediavault-and-tailscale-22g8</link>
      <guid>https://dev.to/fredysandoval/making-a-virtual-nas-with-virt-manager-openmediavault-and-tailscale-22g8</guid>
      <description>&lt;p&gt;TL;DR&lt;br&gt;
This is a practical, guide to turning a spare disk into a NAS by running OpenMediaVault (OMV) inside a KVM virtual machine. It covers the whole path: preparing the disk, installing OMV, passing the disk through correctly (including the traps that are easy to fall into), sharing files over SMB, mounting the share on your host, and reaching your NAS from your phone with Tailscale.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tools used
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;virt-manager&lt;/strong&gt; (libvirt/KVM/QEMU) — the virtualization layer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenMediaVault (OMV)&lt;/strong&gt; — the NAS operating system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;btrfs&lt;/strong&gt; — filesystem, chosen for future expandability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Samba (SMB/CIFS)&lt;/strong&gt; — file sharing protocol&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailscale&lt;/strong&gt; — secure remote access to the NAS from anywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cifs-utils&lt;/strong&gt; — for mounting the share on Linux&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;The VM needs &lt;strong&gt;two disks&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A small &lt;strong&gt;virtual disk&lt;/strong&gt; (qcow2, ~16–20 GB) for the OMV operating system itself&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;physical disk&lt;/strong&gt;, passed through directly to the VM, for your actual data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping these separate matters — OMV refuses to store shared data on its own system disk, and passthrough gives near-native performance on the data disk.&lt;/p&gt;

&lt;p&gt;Important order of operations: &lt;strong&gt;install OMV with only the OS disk attached, and add the physical data disk afterward.&lt;/strong&gt; If both disks are present during install, OMV's partitioner can fail to show a proper disk-selection screen and loop on a "No root file system is defined" error instead. Attaching the data disk after install avoids this entirely.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Place the OMV ISO where libvirt can read it
&lt;/h2&gt;

&lt;p&gt;Download the ISO from &lt;a href="https://www.openmediavault.org/download.html" rel="noopener noreferrer"&gt;https://www.openmediavault.org/download.html&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If virt-manager uses the system session (the default), QEMU runs as its own user and often can't read files inside your home folder. Avoid permission errors by moving the ISO into the default pool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/libvirt/images
&lt;span class="nb"&gt;sudo mv&lt;/span&gt; ~/Downloads/openmediavault_&lt;span class="k"&gt;*&lt;/span&gt;.iso /var/lib/libvirt/images/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Identify your target disk (don't attach it yet)
&lt;/h2&gt;

&lt;p&gt;Find the disk with &lt;code&gt;lsblk&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsblk &lt;span class="nt"&gt;-o&lt;/span&gt; NAME,SIZE,MODEL,MOUNTPOINTS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unmount anything mounted from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;umount /dev/sdX1   &lt;span class="c"&gt;# whatever partitions are mounted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also grab its stable identifier now, so it's ready later — &lt;code&gt;/dev/sdX&lt;/code&gt; names can change across reboots, so use the by-id path instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /dev/disk/by-id/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; part
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the entry that matches your disk (e.g. &lt;code&gt;ata-YourDiskModel_SerialNumber&lt;/code&gt;). You won't attach this disk to the VM until &lt;em&gt;after&lt;/em&gt; OMV is installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Create the VM
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;virt-manager → &lt;strong&gt;File → New Virtual Machine&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local install media (ISO)&lt;/strong&gt; → select the OMV ISO&lt;/li&gt;
&lt;li&gt;Guest OS: &lt;strong&gt;Debian 12&lt;/strong&gt; (OMV 7 is based on it)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg6uik40fyr7g6f7bba5e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg6uik40fyr7g6f7bba5e.png" alt="New virtual machine wizard in virt-manager showing the option to install from local ISO media" width="514" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;RAM: 2048–4096 MB, CPUs: 2 is enough&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnyte1vdto75glqlc78o5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnyte1vdto75glqlc78o5.png" alt="virt-manager wizard step for allocating memory and CPU cores to the new VM" width="509" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a &lt;strong&gt;~16–20 GB&lt;/strong&gt; virtual disk — this becomes the OS disk only (do not attach the physical data disk at this stage)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxtv5ofqiso76bclxzqb0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxtv5ofqiso76bclxzqb0.png" alt="virt-manager wizard step for creating the virtual disk image that will hold the OS" width="510" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Name the VM (e.g. &lt;code&gt;omv&lt;/code&gt;), check &lt;strong&gt;"Customize configuration before install"&lt;/strong&gt; → Finish&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 4: Install OMV
&lt;/h2&gt;

&lt;p&gt;Boot the VM and run through the installer. Since only the OS disk is present, the partitioner will offer just one option — select it, let it guide-partition automatically, set a root password, and complete the install.&lt;/p&gt;

&lt;p&gt;At the console login, the username is &lt;strong&gt;root&lt;/strong&gt;, with the password you set during install — this is separate from the OMV web interface login you'll use later.&lt;/p&gt;

&lt;p&gt;Once you reach the login prompt successfully, shut the VM down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;virsh shutdown omv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Attach the physical data disk
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqt4sbqs00ng2rbvsyaud.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqt4sbqs00ng2rbvsyaud.png" alt="virt-manager steps how to add a new disk" width="800" height="527"&gt;&lt;/a&gt;&lt;br&gt;
Now that OMV is installed, attach the real disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attach it via the command line&lt;/strong&gt; — this avoids a common GUI mistake where selecting "custom storage" and typing just the disk name (without the full path) causes virt-manager to silently create a brand-new empty virtual disk &lt;em&gt;file&lt;/em&gt; instead of using your real disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;virsh attach-disk omv /dev/disk/by-id/ata-YOUR-DISK-ID vdb &lt;span class="nt"&gt;--targetbus&lt;/span&gt; virtio &lt;span class="nt"&gt;--persistent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you prefer the GUI: &lt;strong&gt;Add Hardware → Storage → "Select or create custom storage"&lt;/strong&gt;, then paste the &lt;strong&gt;full absolute path&lt;/strong&gt; (&lt;code&gt;/dev/disk/by-id/...&lt;/code&gt;) into the field, bus type &lt;strong&gt;VirtIO&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Double-check it worked&lt;/strong&gt; by opening the VM's XML (Edit → XML, or &lt;code&gt;virsh dumpxml omv&lt;/code&gt;). You want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;disk&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"block"&lt;/span&gt; &lt;span class="na"&gt;device=&lt;/span&gt;&lt;span class="s"&gt;"disk"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;driver&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"qemu"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"raw"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;dev=&lt;/span&gt;&lt;span class="s"&gt;"/dev/disk/by-id/ata-YOUR-DISK-ID"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;target&lt;/span&gt; &lt;span class="na"&gt;dev=&lt;/span&gt;&lt;span class="s"&gt;"vdb"&lt;/span&gt; &lt;span class="na"&gt;bus=&lt;/span&gt;&lt;span class="s"&gt;"virtio"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/disk&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If instead you see &lt;code&gt;type="file"&lt;/code&gt; and a &lt;code&gt;source file="..."&lt;/code&gt; pointing somewhere in your home folder, that's the mistake above — remove the disk, delete the accidental file it created, and reattach using the full path.&lt;/p&gt;

&lt;p&gt;Boot the VM back up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Log into the web UI
&lt;/h2&gt;

&lt;p&gt;Find the VM's IP:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Browse to &lt;code&gt;http://&amp;lt;vm-ip&amp;gt;&lt;/code&gt;. Default login: &lt;strong&gt;admin / openmediavault&lt;/strong&gt; — change the password immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Wipe and format the data disk (from OMV, not the host)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Storage → Disks&lt;/strong&gt; → select your data disk → &lt;strong&gt;Wipe&lt;/strong&gt; (quick wipe is fine)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage → File Systems → Create&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Select the disk&lt;/li&gt;
&lt;li&gt;Filesystem: &lt;strong&gt;btrfs&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Profile: &lt;strong&gt;single&lt;/strong&gt; (this is the correct choice for one disk — you can convert to a mirror later without reformatting, once you add a second disk)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Mount it when prompted, then apply the pending configuration (the yellow banner at the top — nothing takes effect until you click Apply)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 8: Create a shared folder, user, and SMB share
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Shared folder:&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Storage → Shared Folders → Create&lt;/strong&gt; → name it (e.g. &lt;code&gt;Documents&lt;/code&gt;), pick your btrfs filesystem, save, apply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Users → Users → Create&lt;/strong&gt; → set username/password, add to the &lt;strong&gt;users&lt;/strong&gt; group, shell &lt;code&gt;nologin&lt;/code&gt;, leave SSH keys empty. This user is only for share access — don't reuse the admin account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable SMB:&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Services → SMB/CIFS → Settings&lt;/strong&gt; → check &lt;strong&gt;Enabled&lt;/strong&gt;, leave Workgroup as &lt;code&gt;WORKGROUP&lt;/code&gt;, uncheck Home directories, minimum protocol &lt;code&gt;SMB2&lt;/code&gt;. Save.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create the share:&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Services → SMB/CIFS → Shares → Create&lt;/strong&gt; → pick your shared folder, Public: &lt;code&gt;No&lt;/code&gt;, Browseable: checked, Enable recycle bin: checked. Save → Apply.&lt;/p&gt;

&lt;p&gt;Test from the host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;smbclient &lt;span class="nt"&gt;-L&lt;/span&gt; //&amp;lt;vm-ip&amp;gt; &lt;span class="nt"&gt;-U&lt;/span&gt; yourusername
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see your share listed (alongside &lt;code&gt;IPC$&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Mount the share on your host
&lt;/h2&gt;

&lt;p&gt;For it to behave like a real folder (so &lt;code&gt;touch&lt;/code&gt;, editors, and every app work normally):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-S&lt;/span&gt; cifs-utils   &lt;span class="c"&gt;# or apt install cifs-utils&lt;/span&gt;

&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/nas
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-t&lt;/span&gt; cifs //&amp;lt;vm-ip&amp;gt;/Documents /mnt/nas &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yourusername,uid&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;,gid&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make this permanent and automatic, store the password in a credentials file and use a systemd automount in &lt;code&gt;/etc/fstab&lt;/code&gt;, so the share connects on demand and doesn't hang at boot if the VM isn't running yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/samba-credentials
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;username=yourusername
password=yourpassword
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /etc/samba-credentials
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/etc/fstab&lt;/code&gt; line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//&amp;lt;vm-ip&amp;gt;/Documents  /mnt/nas  cifs  credentials=/etc/samba-credentials,uid=1000,gid=1000,file_mode=0664,dir_mode=0775,noauto,x-systemd.automount,x-systemd.idle-timeout=60,_netdev  0  0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: mounted network shares don't show up in &lt;code&gt;lsblk&lt;/code&gt; (that only lists block devices). Check with &lt;code&gt;findmnt /mnt/nas&lt;/code&gt; or &lt;code&gt;mount | grep cifs&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: Reach your NAS from your phone with Tailscale
&lt;/h2&gt;

&lt;p&gt;NAT networking (virt-manager's default) only lets the &lt;strong&gt;host&lt;/strong&gt; reach the VM. Rather than reconfigure networking with bridges, Tailscale tunnels through NAT with no router or bridge setup needed, and works from anywhere, not just at home.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the VM:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://tailscale.com/install.sh | sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;tailscale up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the printed login URL, sign in, then check the assigned address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailscale ip &lt;span class="nt"&gt;-4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;On your phone:&lt;/strong&gt; install the Tailscale app, log into the same account, enable the VPN toggle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For automatic backup (Android):&lt;/strong&gt; install &lt;strong&gt;FolderSync&lt;/strong&gt;, add an SMB account pointed at the VM's Tailscale IP, and create a folder pair (e.g. &lt;code&gt;DCIM/Camera&lt;/code&gt; → &lt;code&gt;Documents/PhoneBackup&lt;/code&gt;, sync direction "To remote folder"). Schedule it to run daily.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; enable &lt;strong&gt;MagicDNS&lt;/strong&gt; in the Tailscale admin console so you can use a hostname instead of memorizing an IP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping it running
&lt;/h2&gt;

&lt;p&gt;Make the VM start automatically with the host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;virsh autostart omv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Planning ahead for redundancy
&lt;/h2&gt;

&lt;p&gt;Because the data disk was formatted as &lt;strong&gt;btrfs, single profile&lt;/strong&gt;, adding redundancy later is a live, in-place operation — no reformatting or data migration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Attach the new disk to the VM the same way as Step 5 (by-id path, &lt;code&gt;virsh attach-disk&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Wipe it from OMV's disk manager&lt;/li&gt;
&lt;li&gt;Convert to a mirror:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;btrfs device add /dev/vdc /srv/dev-disk-by-uuid-xxxx
btrfs balance start &lt;span class="nt"&gt;-dconvert&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;raid1 &lt;span class="nt"&gt;-mconvert&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;raid1 /srv/dev-disk-by-uuid-xxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your data is then mirrored across both disks. Note this protects against a disk failure — it isn't a substitute for backups against accidental deletion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary of pitfalls this guide avoids
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;ISO unreadable by QEMU → placed in &lt;code&gt;/var/lib/libvirt/images&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Installer failing to show disk selection → install OS with only the OS disk attached; attach the data disk afterward&lt;/li&gt;
&lt;li&gt;Typing a disk name without its full path → accidentally creates a blank virtual disk file instead of using the real disk&lt;/li&gt;
&lt;li&gt;Choosing the wrong filesystem for future growth → btrfs single, upgradeable to raid1 without reformatting&lt;/li&gt;
&lt;li&gt;NAS only reachable from the host → Tailscale, no router/bridge configuration required&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nas</category>
      <category>virtmanager</category>
      <category>openmediavault</category>
      <category>tailscale</category>
    </item>
    <item>
      <title>I'm developing a tool called showsignature [critics wanted]</title>
      <dc:creator>Fredy Sandoval</dc:creator>
      <pubDate>Sat, 11 Jul 2026 03:52:35 +0000</pubDate>
      <link>https://dev.to/fredysandoval/im-developing-a-tool-called-showsignature-critics-wanted-4go1</link>
      <guid>https://dev.to/fredysandoval/im-developing-a-tool-called-showsignature-critics-wanted-4go1</guid>
      <description>&lt;p&gt;Hello Dev Community I'm developing a tool called &lt;code&gt;showsignature&lt;/code&gt; I've been using it to develop itself since the beginning and I've use it on other projects tool, and just feels right.&lt;/p&gt;

&lt;p&gt;It started as specific tool I needed for a particular problem at the beginning, but it was so useful that I adopted it entirely in my code environment with other project, and I took features and added others depending on the need.&lt;/p&gt;

&lt;p&gt;I code mostly with Pi, but I've used Opencode, Codex and Claude code too, and is very useful there as well.&lt;/p&gt;

&lt;p&gt;The tools if fully open-source, and I believe it may be useful to other people too, specially in this times where tokes are becoming more and more expensive each time, and code bases grow and grow, with features here and there.&lt;/p&gt;

&lt;p&gt;It only has 4 dependencies commander, globby, typescript and zod.&lt;/p&gt;

&lt;p&gt;Here is a benchmark for those wondering if this tool is really useful:&lt;/p&gt;

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

&lt;p&gt;For me those results are very impressive because they represent a reduction on %60 token usage and increase in success rate for tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  The way it works is pretty simple:
&lt;/h3&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;showsignature map  &lt;span class="o"&gt;[&lt;/span&gt;OPTION]... &lt;span class="o"&gt;[&lt;/span&gt;PATH]...
showsignature &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;OPTION]... &amp;lt;FILE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has two commands:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;map&lt;/strong&gt; — structural overview: signatures and other extracted entries. Inspect [PATH] operands—files or directory paths—using the current directory by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;read&lt;/strong&gt; — literal windowed read of exactly one file, with an optional structural outline around the window for orientation.&lt;br&gt;
Running showsignature with no command prints help and exits with code 1.&lt;/p&gt;

&lt;p&gt;For Pi and Opencode it works as a native tool call, for Claude code and Codex is used as a bash command.&lt;/p&gt;

&lt;p&gt;So if you want to give a try, I'm fully opened to critics, bug reports and pull requests, if you haven't contribute to opensource and need that for your resume, you're also welcome.&lt;/p&gt;

&lt;p&gt;vibe-testing.&lt;sup id="fnref1"&gt;1&lt;/sup&gt; is also welcome.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/FredySandoval/showsignature" rel="noopener noreferrer"&gt;https://github.com/FredySandoval/showsignature&lt;/a&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Vibe-testing: asking your agent to use a tool or command exhaustively until it find a strange behavior or bug and report.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>cli</category>
      <category>tooling</category>
      <category>agents</category>
      <category>analytics</category>
    </item>
    <item>
      <title>A Pi extension to show a new Pi logo every time you open Pi.</title>
      <dc:creator>Fredy Sandoval</dc:creator>
      <pubDate>Tue, 12 May 2026 08:03:55 +0000</pubDate>
      <link>https://dev.to/fredysandoval/a-pi-extension-to-show-a-new-pi-logo-every-time-you-open-pi-3407</link>
      <guid>https://dev.to/fredysandoval/a-pi-extension-to-show-a-new-pi-logo-every-time-you-open-pi-3407</guid>
      <description>&lt;p&gt;People of pi, I Just published a Pi extension to show a new Pi logo every time you open Pi.&lt;/p&gt;

&lt;p&gt;What you get&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Random Pi logo on startup&lt;/li&gt;
&lt;li&gt;Small animation when Pi opens a new session&lt;/li&gt;
&lt;li&gt;Built-in shortcut help in the header&lt;/li&gt;
&lt;li&gt;Responsive layout for narrow terminals&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/FredySandoval" rel="noopener noreferrer"&gt;
        FredySandoval
      &lt;/a&gt; / &lt;a href="https://github.com/FredySandoval/pi-logo" rel="noopener noreferrer"&gt;
        pi-logo
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A Pi extension to show a new Pi logo every time you open Pi.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;pi-logo&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;A &lt;code&gt;Pi&lt;/code&gt; extension to show a new &lt;code&gt;Pi&lt;/code&gt; logo every time you open &lt;code&gt;Pi.&lt;/code&gt;&lt;/p&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/45242501/590383732-2c868585-7edb-439a-bb18-7177e103a976.gif?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODM3NDI1NDMsIm5iZiI6MTc4Mzc0MjI0MywicGF0aCI6Ii80NTI0MjUwMS81OTAzODM3MzItMmM4Njg1ODUtN2VkYi00MzlhLWJiMTgtNzE3N2UxMDNhOTc2LmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA3MTElMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNzExVDAzNTcyM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWViYzcxZTNlYjAxM2UxY2UwYjY5NjZjYmJhY2QyNjA1MzA2NTVhOTM0Y2Y0YzIzZjM4ZWQzYjMyYzAwYTA4ZWQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRmdpZiJ9.1Xx9b_gdD-j7QveSDyyWUfYDtw0Iwc8mhJj_jSMkNm0"&gt;&lt;img width="960" height="530" alt="2026-05-10 19-00-42_trimmed" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F45242501%2F590383732-2c868585-7edb-439a-bb18-7177e103a976.gif%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODM3NDI1NDMsIm5iZiI6MTc4Mzc0MjI0MywicGF0aCI6Ii80NTI0MjUwMS81OTAzODM3MzItMmM4Njg1ODUtN2VkYi00MzlhLWJiMTgtNzE3N2UxMDNhOTc2LmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA3MTElMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNzExVDAzNTcyM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWViYzcxZTNlYjAxM2UxY2UwYjY5NjZjYmJhY2QyNjA1MzA2NTVhOTM0Y2Y0YzIzZjM4ZWQzYjMyYzAwYTA4ZWQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRmdpZiJ9.1Xx9b_gdD-j7QveSDyyWUfYDtw0Iwc8mhJj_jSMkNm0" class="js-gh-image-fallback"&gt;&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;pi install npm:pi-logo&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Restart Pi.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What you get&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Random Pi logo on startup&lt;/li&gt;
&lt;li&gt;Small animation when Pi opens a new session&lt;/li&gt;
&lt;li&gt;Built-in shortcut help in the header&lt;/li&gt;
&lt;li&gt;Responsive layout for narrow terminals&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How to use?&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Just open Pi! and enjoy a new logo everytime you open &lt;code&gt;Pi.&lt;/code&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Try without installing&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;pi -e npm:pi-logo&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Remove&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;pi remove npm:pi-logo&lt;/pre&gt;

&lt;/div&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/45242501/590380323-e86abc1e-444d-41bb-9cfc-65c258173db7.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODM3NDI1NDMsIm5iZiI6MTc4Mzc0MjI0MywicGF0aCI6Ii80NTI0MjUwMS81OTAzODAzMjMtZTg2YWJjMWUtNDQ0ZC00MWJiLTljZmMtNjVjMjU4MTczZGI3LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA3MTElMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNzExVDAzNTcyM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWNhZWQ3YmI4NzU0NzA1NGUyMTQ1NjVkZDcwZmRjNTk3YzhkNzEwZDVjYTdhYWVjNTg5MDEwZDYyZGZhOTVhYTYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRnBuZyJ9.F38Jy1Wl1NnMkvxg-EKTb6MuTzXC7o9zoX9o8LTBuFY"&gt;&lt;img width="898" height="157" alt="image" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F45242501%2F590380323-e86abc1e-444d-41bb-9cfc-65c258173db7.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODM3NDI1NDMsIm5iZiI6MTc4Mzc0MjI0MywicGF0aCI6Ii80NTI0MjUwMS81OTAzODAzMjMtZTg2YWJjMWUtNDQ0ZC00MWJiLTljZmMtNjVjMjU4MTczZGI3LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA3MTElMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNzExVDAzNTcyM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWNhZWQ3YmI4NzU0NzA1NGUyMTQ1NjVkZDcwZmRjNTk3YzhkNzEwZDVjYTdhYWVjNTg5MDEwZDYyZGZhOTVhYTYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRnBuZyJ9.F38Jy1Wl1NnMkvxg-EKTb6MuTzXC7o9zoX9o8LTBuFY" class="js-gh-image-fallback"&gt;&lt;/a&gt;

&lt;div class="highlight highlight-source-assembly notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-en"&gt;█▀█  pi&lt;/span&gt;&lt;span class="pl-s1"&gt;-&lt;/span&gt;&lt;span class="pl-en"&gt;logo v1.&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-en"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;
&lt;span class="pl-en"&gt;█▀ █ Oh by the way&lt;/span&gt;&lt;span class="pl-s1"&gt;,&lt;/span&gt;&lt;span class="pl-en"&gt; which one’s Pink?&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/FredySandoval/pi-logo" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>pi</category>
      <category>extensions</category>
      <category>pimono</category>
      <category>harness</category>
    </item>
    <item>
      <title>Terminal Tip 🤩 fcopy, fcut, fpaste</title>
      <dc:creator>Fredy Sandoval</dc:creator>
      <pubDate>Sun, 15 Mar 2026 02:47:00 +0000</pubDate>
      <link>https://dev.to/fredysandoval/terminal-tip-fcopy-fcut-fpaste-4nfg</link>
      <guid>https://dev.to/fredysandoval/terminal-tip-fcopy-fcut-fpaste-4nfg</guid>
      <description>&lt;p&gt;If you are like me and love the terminal, &lt;strong&gt;start using&lt;/strong&gt; &lt;code&gt;fcopy&lt;/code&gt;, &lt;code&gt;fcut&lt;/code&gt;, &lt;code&gt;fpaste&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;how does it work, instead of copying through a file explorer, just do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;fcopy some/file/
&lt;span class="nv"&gt;$ &lt;/span&gt;fcopy other/file/image.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then open other terminal and do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;fpaste
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fcopy&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"$#"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"fcopy: nothing to copy"&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="k"&gt;fi

  &lt;/span&gt;&lt;span class="nv"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'copy\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;item &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nv"&gt;abs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;realpath&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$abs&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"copied to clipboard: &lt;/span&gt;&lt;span class="nv"&gt;$abs&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"fcopy: not found: &lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="k"&gt;fi
  done

  &lt;/span&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ~/.fileclip
&lt;span class="o"&gt;}&lt;/span&gt;

fcut&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"$#"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"fcut: nothing to cut"&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="k"&gt;fi

  &lt;/span&gt;&lt;span class="nv"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'move\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;item &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nv"&gt;abs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;realpath&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$abs&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"cut to clipboard: &lt;/span&gt;&lt;span class="nv"&gt;$abs&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"fcut: not found: &lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="k"&gt;fi
  done

  &lt;/span&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ~/.fileclip
&lt;span class="o"&gt;}&lt;/span&gt;

fpaste&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.fileclip &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"fpaste: file clipboard is empty"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt; ~/.fileclip&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; src&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue

    if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"fpaste: source no longer exists: &lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
      &lt;span class="k"&gt;continue
    fi

    if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$mode&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"move"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      if &lt;/span&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; .&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"moved: &lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;
        &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;count &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;else
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"fpaste: failed to move: &lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
      &lt;span class="k"&gt;fi
    else
      if &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; .&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"pasted: &lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;
        &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;count &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;else
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"fpaste: failed to paste: &lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
      &lt;span class="k"&gt;fi
    fi
  done&lt;/span&gt; &amp;lt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; +2 ~/.fileclip&lt;span class="o"&gt;)&lt;/span&gt;

  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"done: &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt; item(s)"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>terminal</category>
      <category>files</category>
      <category>tip</category>
      <category>zsh</category>
    </item>
    <item>
      <title>If you like VS Code REST Client, you're going to love LAZYREQUEST</title>
      <dc:creator>Fredy Sandoval</dc:creator>
      <pubDate>Wed, 25 Feb 2026 01:41:04 +0000</pubDate>
      <link>https://dev.to/fredysandoval/if-you-like-vs-code-rest-client-youre-going-to-love-lazyrequest-m57</link>
      <guid>https://dev.to/fredysandoval/if-you-like-vs-code-rest-client-youre-going-to-love-lazyrequest-m57</guid>
      <description>&lt;h2&gt;
  
  
  If you like VS Code REST Client, you're going to love LAZYREQUEST
&lt;/h2&gt;

&lt;p&gt;You already know the best format for API testing: a plain &lt;code&gt;.http&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;What you &lt;em&gt;probably&lt;/em&gt; don't have is a dead-simple way to run those files from the terminal (and CI), validate the responses you expect, and get a clean PASS/FAIL signal without adopting an entire new testing framework.&lt;/p&gt;

&lt;p&gt;Meet &lt;strong&gt;LAZYREQUEST&lt;/strong&gt;: a lightweight, minimal API Testing Client CLI tool inspired by &lt;strong&gt;VS Code REST Client syntax&lt;/strong&gt;. It recursively discovers &lt;code&gt;.http&lt;/code&gt; and &lt;code&gt;.rest&lt;/code&gt; files, sends the requests, and compares actual responses against the expected responses you declare in the same template.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lazyrequest.fredy.dev" rel="noopener noreferrer"&gt;Site&lt;/a&gt; | &lt;a href="https://www.npmjs.com/package/lazyrequest" rel="noopener noreferrer"&gt;npm&lt;/a&gt; | &lt;a href="https://github.com/FredySandoval/lazyrequest" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;If you've ever thought:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"My API smoke tests should be as easy as writing an HTTP request."&lt;/li&gt;
&lt;li&gt;"I want contract-ish checks without standing up a whole test harness."&lt;/li&gt;
&lt;li&gt;"Why does 'try this endpoint' live in Slack instead of in the repo?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is for YOU!.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Write requests the same way you already do in VS Code REST Client: method, URL, headers, body.&lt;/li&gt;
&lt;li&gt;Optionally declare an expected response block right in the file (status + headers + body).&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run &lt;code&gt;lazyrequest&lt;/code&gt; on a folder. It finds everything, executes fast (concurrent by default), and tells you what broke.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No new syntax.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No heavy GUI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No learning curve.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No Postman tab explosion.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just .http files. And power.&lt;/p&gt;

&lt;p&gt;No collections. No click-ops. No "where did we store the latest curl command?"&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Let's say you are working on a API and you need to test it, you can quickly write a &lt;code&gt;.http&lt;/code&gt; file with something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET http://localhost:8080/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see it's output. It works. It’s clean. It’s readable.&lt;/p&gt;

&lt;p&gt;But then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You need CI integration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You want automated assertions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You want folder-wide execution&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You want concurrent runs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You don’t want a browser UI&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suddenly you’re installing a whole testing framework. Yikes...&lt;/p&gt;




&lt;h2&gt;
  
  
  API Testing: when testing becomes a torture
&lt;/h2&gt;

&lt;p&gt;Most teams already have "API tests" scattered across:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;random curl commands in docs&lt;/li&gt;
&lt;li&gt;unreadable Postman collections everybody is affraid to touch&lt;/li&gt;
&lt;li&gt;a half-finished test suite that only one person understands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LAZYREQUEST makes the happy path boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your requests live in the repo as &lt;code&gt;.http&lt;/code&gt; / &lt;code&gt;.rest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;your expectations live next to the request (when you want assertions)&lt;/li&gt;
&lt;li&gt;your terminal becomes the source of truth&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A real example (request + expected response)
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What If Your .http Files Became Your Test Suite?
&lt;/h2&gt;

&lt;p&gt;That’s exactly what LAZYREQUEST does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;@baseUrl = http://localhost:8080

###
&lt;/span&gt;&lt;span class="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;{{baseUrl}}/users&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Doe"&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="s2"&gt;"john@example.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;###&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Expected&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;response&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;HTTP/&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Created&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Content-Type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;application/json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Location:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="err"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;/users/&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;123&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;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;lazyrequest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you instantly know:&lt;br&gt;
✓ Passed/Failed Status&lt;br&gt;
✓ Headers correct&lt;br&gt;
✓ JSON structure correct&lt;/p&gt;

&lt;p&gt;Or exactly what failed.&lt;/p&gt;

&lt;p&gt;Now your repo contains something you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;review in a PR&lt;/li&gt;
&lt;li&gt;run locally&lt;/li&gt;
&lt;li&gt;run in CI&lt;/li&gt;
&lt;li&gt;keep up-to-date without ceremony&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Try It. Break It. Star It. ⭐
&lt;/h2&gt;

&lt;p&gt;Installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; lazyrequest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or clone + build with Bun:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/FredySandoval/lazyrequest.git
bun build ./src/index.ts &lt;span class="nt"&gt;--compile&lt;/span&gt; &lt;span class="nt"&gt;--minify&lt;/span&gt; &lt;span class="nt"&gt;--outfile&lt;/span&gt; lazyrequest &lt;span class="nt"&gt;--target&lt;/span&gt; bun
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or download the binary from GitHub releases.&lt;br&gt;
&lt;a href="https://github.com/FredySandoval/lazyrequest" rel="noopener noreferrer"&gt;https://github.com/FredySandoval/lazyrequest&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;npm:&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/lazyrequest" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/lazyrequest&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The best tools don’t invent new syntax.&lt;br&gt;
They unlock the power of what you’re already using.&lt;br&gt;
LAZYREQUEST doesn’t replace your workflow.&lt;br&gt;
It upgrades it. 🚀&lt;/p&gt;

</description>
      <category>http</category>
      <category>rest</category>
      <category>graphql</category>
      <category>markdown</category>
    </item>
    <item>
      <title>Google OAuth2 for Dummies</title>
      <dc:creator>Fredy Sandoval</dc:creator>
      <pubDate>Mon, 12 Jun 2023 18:07:27 +0000</pubDate>
      <link>https://dev.to/fredysandoval/google-oauth2-for-dummies-4bl1</link>
      <guid>https://dev.to/fredysandoval/google-oauth2-for-dummies-4bl1</guid>
      <description>&lt;p&gt;The Full code is at the bottom. &lt;/p&gt;

&lt;p&gt;TLDR: In this tutorial we will Authenticate an Application using Google OAuth2. It loads credentials from a local JSON file, creates an OAuth2 client with those credentials, and sets up a few HTTP routes to handle the authentication process.&lt;/p&gt;

&lt;p&gt;Questions to be answered: &lt;/p&gt;

&lt;h4&gt;
  
  
  1. How authenticate an app using Google OAuth2 in NodeJS?
&lt;/h4&gt;

&lt;p&gt;You use the googleapis npm package to create an OAuth2 client with your app's credentials. Generate an authorization URL and redirect users there to grant your app permissions. When they're redirected back to your app, exchange the "code" they bring for access and refresh tokens.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. How do you load credentials from a JSON file in Google APIs?
&lt;/h4&gt;

&lt;p&gt;You can use the fs module's readFileSync function to read the contents of the file. Then, use JSON.parse to parse the contents into a JavaScript object.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. What are the Google API scopes?
&lt;/h4&gt;

&lt;p&gt;Scopes define the permissions your app is requesting from users. This script requests permission to send Gmail messages on the user's behalf.&lt;/p&gt;

&lt;h2&gt;
  
  
  PART 1
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Creating credentials
&lt;/h3&gt;

&lt;p&gt;a. Go to Google Console &amp;gt; Credentials&lt;br&gt;
&lt;a href="https://console.cloud.google.com/apis/credentials" rel="noopener noreferrer"&gt;https://console.cloud.google.com/apis/credentials&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;b. Create a new Credential of type OAuth client ID&lt;/p&gt;

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

&lt;p&gt;c. Select type Desktop and give any name you want&lt;/p&gt;

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

&lt;p&gt;d. Save the JSON file (client_secret_xxx…) in the root folder of&lt;br&gt;
the project&lt;/p&gt;

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

&lt;p&gt;You must add the email addresses of the people that will use&lt;br&gt;
the application, including yours. Otherwise that person won’t&lt;br&gt;
be able to use the application.&lt;/p&gt;

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

&lt;p&gt;You also must modify the JSON secret file (the file you just&lt;br&gt;
downloaded "client_secret_xxx…")&lt;br&gt;
At the end of the file you will find “redirect_uris”, you need to&lt;br&gt;
modify it like this:&lt;/p&gt;

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

&lt;p&gt;We're going to break down a piece of code that allows your application to interact with Google APIs using OAuth2 for authentication. This script involves several important Node.js modules like &lt;code&gt;googleapis&lt;/code&gt;, &lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;, and &lt;code&gt;express&lt;/code&gt;. Let's get started!&lt;/p&gt;

&lt;p&gt;First off, install the necessary packages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;express googleapis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;now let's import some essential modules:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;googleapis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&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;code&gt;google&lt;/code&gt; is part of &lt;code&gt;googleapis&lt;/code&gt;, which lets us interact with Google's services. &lt;code&gt;fs&lt;/code&gt; is a built-in Node.js module to work with the file system, and &lt;code&gt;path&lt;/code&gt; helps us with file paths.&lt;/p&gt;

&lt;p&gt;Now, we define some constants:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CREDENTIALS_FOLDER&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// location of credentials&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SCOPES&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.googleapis.com/auth/gmail.send&lt;/span&gt;&lt;span class="dl"&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;code&gt;CREDENTIALS_FOLDER&lt;/code&gt; is the location of our Google API credentials file. &lt;code&gt;SCOPES&lt;/code&gt; is an array that specifies what permissions our application requests from users. Here, we're asking to send Gmail messages on behalf of the user.&lt;/p&gt;

&lt;p&gt;The next part of the script involves loading our Google API credentials:&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CREDENTIALS_FOLDER&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;credentialsFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_secret_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credentialsPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CREDENTIALS_FOLDER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;credentialsFile&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;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credentialsPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;credentials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unable to read file, can't continue&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;This script looks for a file that starts with &lt;code&gt;client_secret_&lt;/code&gt; and ends with &lt;code&gt;.json&lt;/code&gt; (this is your Google API credentials file). It then reads the file and parses it as JSON. If something goes wrong (like if the file doesn't exist), it logs an error message.&lt;/p&gt;

&lt;p&gt;Next, we validate our credentials:&lt;br&gt;
But why? it is very common that we or somebody loads the incorrect credentials type, Google has different credentials, and validating we have the right ones will save hours of debugging.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REQUIRED_CREDENTIALS_PROPERTIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;project_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth_uri&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth_provider_x509_cert_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token_uri&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redirect_uris&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nx"&gt;REQUIRED_CREDENTIALS_PROPERTIES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prop&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prop&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; 
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Property &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; not found in credentials`&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;We have a list of all the properties that should be in our credentials object, and we make sure that each one is there. If not, we throw an error.&lt;/p&gt;

&lt;p&gt;Now we create an OAuth2 client:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;redirect_uris&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;credentials&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;oAuth2Client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OAuth2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;redirect_uris&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;authUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;oAuth2Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateAuthUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;access_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;offline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SCOPES&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We pull out the necessary parts of our credentials and create an OAuth2 client. We then generate a URL &lt;code&gt;(authUrl)&lt;/code&gt; where users will grant our application the permissions we requested.&lt;/p&gt;

&lt;p&gt;Next, we set up an Express application:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Express is a framework that makes it easier to handle HTTP requests and responses in Node.js.&lt;/p&gt;

&lt;p&gt;We define three routes. The first &lt;code&gt;('/')&lt;/code&gt; sends users to the &lt;code&gt;authUrl&lt;/code&gt; we generated earlier:&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;a href="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;Authorize&amp;lt;/a&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second (/oauth2callback) is the "callback URL". After users grant permissions, Google redirects them here with a special "code". We exchange that code for access and refresh tokens, which allow us to make authorized requests on behalf of the user:&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/oauth2callback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;oAuth2Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;oAuth2Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCredentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/authenticated&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;The third route &lt;code&gt;('/authenticated')&lt;/code&gt; simply confirms that the user has been authenticated:&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/authenticated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are authenticated now!&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;Next, we define two "middleware" functions to handle requests that don't match any routes and to handle errors:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&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;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Not Found - &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;errorHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errorHandler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These functions provide helpful error messages to users and developers.&lt;/p&gt;

&lt;p&gt;Finally, we make our app listen on a specific port:&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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;interfaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;os&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;networkInterfaces&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;localhostIP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;interfaces&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;k2&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;interfaces&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ipFamily&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interfaces&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;k2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;family&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;ipFamily&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IPv4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;ipFamily&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;interfaces&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;k2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;internal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;localhostIP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interfaces&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;k2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Listening on http://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;localhostIP&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script starts our app listening on the port specified by the environment variable &lt;code&gt;PORT&lt;/code&gt;, or 5000 if &lt;code&gt;PORT&lt;/code&gt; isn't defined. The server's address is logged to the console when the server starts.&lt;/p&gt;

&lt;p&gt;And that's it! You've just learned how to set up a simple OAuth2 authentication flow with Google APIs and Express. It's not so difficult when you break it down, right? Happy coding!&lt;/p&gt;

&lt;h3&gt;
  
  
  Full code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;googleapis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;                  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;                &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CREDENTIALS_FOLDER&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SCOPES&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.googleapis.com/auth/gmail.send&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="cm"&gt;/* GLOBAL VARIABLES*/&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="c1"&gt;// ----------  OAUTH2 CLIENT ----------------&lt;/span&gt;
&lt;span class="c1"&gt;// Load client secrets into "credentials"&lt;/span&gt;
&lt;span class="k"&gt;try&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;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CREDENTIALS_FOLDER&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;credentialsFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_secret_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;credentialsFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;credentials file not found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// const credentialsPath = CREDENTIALS_FOLDER + credentialsFile;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credentialsPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CREDENTIALS_FOLDER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;credentialsFile&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;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credentialsPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;credentials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unable to read file, can't continue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// VALIDATE CREDENTIALS &lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REQUIRED_CREDENTIALS_PROPERTIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;project_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth_uri&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth_provider_x509_cert_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token_uri&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redirect_uris&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Ensure all required properties exist&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;credentials or installed not found in credentials&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;REQUIRED_CREDENTIALS_PROPERTIES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prop&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prop&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; 
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Property &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; not found in credentials`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;redirect_uris&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;credentials&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;oAuth2Client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OAuth2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;redirect_uris&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;authUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;oAuth2Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateAuthUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;access_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;offline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SCOPES&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ----------  EXPRESS APPLICATION  ----------------&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;a href="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;Authorize&amp;lt;/a&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/oauth2callback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;oAuth2Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;oAuth2Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCredentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/authenticated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/authenticated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are authenticated now!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// ----------  EXPRESS FALLBACKS  ----------------&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&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;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Not Found - &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;errorHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errorHandler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// gets the localhost IP address&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;interfaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;os&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;networkInterfaces&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;localhostIP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;interfaces&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;k2&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;interfaces&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ipFamily&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interfaces&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;k2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;family&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;ipFamily&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IPv4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;ipFamily&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;interfaces&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;k2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;internal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;localhostIP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interfaces&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;k2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Listening on http://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;localhostIP&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>javascript</category>
      <category>tutorial</category>
      <category>oauth</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Explaining the video "Enums Considered Harmful" of Matt Pocock</title>
      <dc:creator>Fredy Sandoval</dc:creator>
      <pubDate>Sun, 18 Dec 2022 04:16:14 +0000</pubDate>
      <link>https://dev.to/fredysandoval/explaining-the-video-enums-considered-harmful-of-matt-pocock-2a20</link>
      <guid>https://dev.to/fredysandoval/explaining-the-video-enums-considered-harmful-of-matt-pocock-2a20</guid>
      <description>&lt;p&gt;I was watching this &lt;a href="https://www.youtube.com/watch?v=jjMbPt_H3RQ&amp;amp;t" rel="noopener noreferrer"&gt;video&lt;/a&gt; by Matt Pocock, but I found really hard to digest and internalize the TypeScript, so I decided to create a little guide, because I learn better when I explain things to others, so here it is.&lt;br&gt;
&lt;strong&gt;The TypeScript shown in the video&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="na"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;debug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;WARNING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;ObjectValues&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&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;LogLevel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ObjectValues&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;







&lt;h2&gt;
  
  
  Destructuring...
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;'typeof' will get you the types&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL_TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Hardcoded will be like this:&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL_TYPE_2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;debug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;WARNING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&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;'keyof' will get the keys&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL_KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL_TYPE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Hardcoded will be like this:&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL_KEYS_2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ERROR&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;WARNING&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;DEBUG&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;'type[i]' will get the values&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL_VALUES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL_TYPE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ERROR&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;WARNING&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;DEBUG&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="c1"&gt;// or&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL_VALUES_2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL_TYPE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;LOG_LEVEL_KEYS&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="c1"&gt;// Hardcoded will be like this:&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL_VALUES_3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;debug&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;warning&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;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For example the variable myLog can only be the VALUES, anything else will throw an error&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;myLog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL_VALUES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;myLog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;debug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;myLog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;myLog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;MyLog_2 can only be the KEYS&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;myLog_2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL_KEYS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;myLog_2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DEBUG&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;myLog_2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ERROR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;myLog_2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WARNING&lt;/span&gt;&lt;span class="dl"&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;The previous can also be compressed and written like this:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL_VALUES_4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="c1"&gt;// Hardcoded will be like this:&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL_VALUES_5&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;debug&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;warning&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;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;







&lt;h2&gt;
  
  
  Now explaining the TypeScript will be:
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ObjectValues&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Imagine it as a function that returns the values of wherever T is.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ObjectValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;for example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MyType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="na"&gt;myString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;myNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;myValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;ObjectValues&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MyType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;myValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// can only be the litteral string,&lt;/span&gt;
&lt;span class="nx"&gt;myValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// or the litteral value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Explaining the last part&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LogLevel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ObjectValues&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;// Now imagine this part like we actually execute the function and &lt;/span&gt;
&lt;span class="c1"&gt;// we passed the types provided by "typeof"&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;logLevel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ObjectValues&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;myString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my string&lt;/span&gt;&lt;span class="dl"&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;a href="https://www.typescriptlang.org/play?#code/MYewdgzgLgBAMgeQOIH04FEBq64wLwwDeAUDGQCLoBCAqkgFwwDkAJgKYBGArgOZMA0pMgHUAggCUAcgElJDZgHcAhgCcwASzB9BZGOnHiE4xkzYqVIFQOIBfGEogxQkKAG5ipAPQAqMgBUATQAFdAQAMRgFdQAbaJgeNlgoAAs2GCgATwAHNJhvT2JMnPhkNCwcFECQ-HTsthAAMxLUDGw4d09PGAAJVRZQdhZImLiONOj1AGs0lPUIekK65rK2yuD0FAAmGpJdGBU2JRZwaIyYSlp5ACJ2bh4r9z2Do5OzsSlZa+U1TXvH3WexzApz0BiMjCuZgsKgetg8MB8ZAA0ugAuFhrF4ol0qkYNMMo58otiogWuU4CgUQEAMo1fGNZatCpVdAdLrSRwpNIQJQAWzSDkiKnUUDYCyKaVJKwqVOpWxqV30hnEVxgAB8YFd3jI5KqNVcLnQrh5iIiWQBtdQAXQxcQSSVxADclNEuGxCQUJYzyShMKI4DR0LSCFKmRSLYqwSr1ZrtZ89ZrDUgrla2TBLMTJaUw77-YG5dsQ9mfRbQz7ZanTV1eip+iBBraYGMYBNpji5uKlmXVn6A0GUABmBW3XgJq7fDRaMdQyzG4jRbG8jJwEA8Rjdiq9-OuBFdJcrnhOJRgdPAs7NrkwLdB4j71c1VicXhMdx3w8EJgT34v2-L+8fmcrHcedFz-HgtnXYtVllHdOhgABZMD5WAY9TxBC9cVlX8D3lD8kx-N9cOYJUjAIpDC2YOM5B-KsYD8XEsgOR11BALhHBQk8XQgEAmzSUBeUY90IDYIZjyGBRhSgUUT1bGZkg7TNvR7PN+wAFhqCUGQ3OBzXpJpNKabTKzgms6wbKJMWbWT23mRTtNzPs5QAVmHJ97hjcdVEndz9UAuc4PINh+KyEAIF+RsxgWRSEA4AArIKoEwF03QgAAePwAD4aj8XS2AyBk-ErWjpF5JQeE0NIRXsRwlBgBouDAYAoBYk8UiUWADigLg1E5J1kvddMmgUVImLMOiYDmAA6Yh6sa5rwBgGL4qapLXXdAAKPx6GPDIAEoiCEMhOu6k8cqWhLJvxCBNt280AAYrStOEPDghpLBgNgAA8+SyBdO2KRC-CWAhdjIJdqSgYUtBMJcYGgKHtEOpdJC4XkxhURhNgHQQbA8BdYCXVa3Xoc6Vv6tLAbqDLXwyIm0g-WH4e-XcjxPV5eJxcYRVFFQXThyHfkEQn+pqLGdz2PY4Pey8Jiksw+edNaPC9A84DYR02DiAhScS8nUoMpScAy2jJBABQJtK8qwDkuYYCyVRYGshQBSargXRBL6gq4UVObqhqmpa+wwCGWjnbthxhKGS8JUcRiQGYhsODOK4DONfGW1XNWNa1xa4oSunrsIGBwYF6HmEZ0u+BsXbiCAA" rel="noopener noreferrer"&gt;Playground Link&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>TypeScript: A beginner's guide with emojis 🤗</title>
      <dc:creator>Fredy Sandoval</dc:creator>
      <pubDate>Wed, 07 Dec 2022 22:02:32 +0000</pubDate>
      <link>https://dev.to/fredysandoval/typescript-a-beginners-guide-with-emojis-21cj</link>
      <guid>https://dev.to/fredysandoval/typescript-a-beginners-guide-with-emojis-21cj</guid>
      <description>&lt;p&gt;Is basically telling what type something is.&lt;/p&gt;

&lt;p&gt;For example let say we have a box,&lt;br&gt;
and we want to tell people to only put eggs in the box.&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;myBox&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 📦&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In real life we would put a sticker in the box saying "only eggs &lt;br&gt;
please 📄", in TypeScript we do the same, we add a "type" saying what something is and only accepts.&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="nx"&gt;Variable&lt;/span&gt;           &lt;span class="nx"&gt;Type&lt;/span&gt;            &lt;span class="nx"&gt;value&lt;/span&gt;
&lt;span class="err"&gt;┌────┴─────┐&lt;/span&gt;     &lt;span class="err"&gt;┌────┴─────┐&lt;/span&gt;      &lt;span class="err"&gt;┌───┴───┐&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;myBox_&lt;/span&gt;&lt;span class="err"&gt;📦&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;onlyEggs_&lt;/span&gt;&lt;span class="err"&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;🥚🥚🥚&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
               &lt;span class="err"&gt;↑&lt;/span&gt;                &lt;span class="err"&gt;↑&lt;/span&gt;
               &lt;span class="nx"&gt;Separator&lt;/span&gt;        &lt;span class="nx"&gt;Assignment&lt;/span&gt; &lt;span class="nx"&gt;operator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are 3 basic types in TypeScript&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isDone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// 😀 or 😟&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// 0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣...&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;John&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// 📃&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When it's impossible to know, there is the "Any" type&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;notSure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                     &lt;span class="c1"&gt;// 🤷‍♂️ Not sure&lt;/span&gt;

&lt;span class="nx"&gt;notSure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;I'm a string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// I can change it later&lt;/span&gt;

&lt;span class="nx"&gt;notSure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                         &lt;span class="c1"&gt;// maybe a boolean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are typed arrays&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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="c1"&gt;// or with emojis:&lt;/span&gt;
        &lt;span class="nx"&gt;Only&lt;/span&gt; &lt;span class="nx"&gt;chickens&lt;/span&gt; &lt;span class="nx"&gt;please&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
              &lt;span class="err"&gt;┌─┴─┐&lt;/span&gt; 
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;chickens&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="o"&gt;=&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="err"&gt;🐤&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="err"&gt;🐓&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively you can use Array which It's same as Type[]&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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="c1"&gt;// or with emojis&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;listOfChickens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;🐔&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&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="err"&gt;🐤&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="err"&gt;🐓&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enumerations also known as enums:&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="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;Square&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Red&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Green&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Blue&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;         &lt;span class="c1"&gt;// 🟥, 🟩, 🟦&lt;/span&gt;

&lt;span class="c1"&gt;// This can be understood better by seeing what Enumerations &lt;/span&gt;
&lt;span class="c1"&gt;// are compiled to in JavaScript:&lt;/span&gt;
&lt;span class="nx"&gt;Square&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
           &lt;span class="mi"&gt;0&lt;/span&gt;       &lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Red&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
           &lt;span class="mi"&gt;1&lt;/span&gt;       &lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Green&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
           &lt;span class="mi"&gt;2&lt;/span&gt;       &lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Blue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
           &lt;span class="na"&gt;Red&lt;/span&gt;     &lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
           &lt;span class="na"&gt;Green&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
           &lt;span class="na"&gt;Blue&lt;/span&gt;    &lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Now that you know is just an object, &lt;/span&gt;
&lt;span class="c1"&gt;// you can access it by name or number.&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;Square&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Green&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;//  🟩&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;Square&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;                 &lt;span class="c1"&gt;//  🟥 &lt;/span&gt;

&lt;span class="c1"&gt;// or in a more complex way&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Square&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Square&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Blue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;Square&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;                 &lt;span class="c1"&gt;//  🟦&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please Comment if you want to see more content like this!&lt;br&gt;
👇 Follow me on Twitter&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/fsandovaldev" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Ftwitter%2Furl.svg%3Flabel%3DFollow%2520%2540fsandovaldev%26style%3Dsocial%26url%3Dhttps%253A%252F%252Ftwitter.com%252Ffsandovaldev" alt="Twitter" width="143" height="20"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>softwareengineering</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
