<?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: Adele Beitvashahi</title>
    <description>The latest articles on DEV Community by Adele Beitvashahi (@adelbeit).</description>
    <link>https://dev.to/adelbeit</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%2F964746%2F51a37dc3-e2af-4c01-b6c7-f561cf55c12e.jpeg</url>
      <title>DEV Community: Adele Beitvashahi</title>
      <link>https://dev.to/adelbeit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adelbeit"/>
    <language>en</language>
    <item>
      <title>Deploy your own database as a remote backend service in less than an hour</title>
      <dc:creator>Adele Beitvashahi</dc:creator>
      <pubDate>Mon, 19 Oct 2026 00:26:05 +0000</pubDate>
      <link>https://dev.to/adelbeit/deploy-your-own-pocketbase-backend-to-a-remote-env-in-less-than-an-hour-4e9c</link>
      <guid>https://dev.to/adelbeit/deploy-your-own-pocketbase-backend-to-a-remote-env-in-less-than-an-hour-4e9c</guid>
      <description>&lt;h2&gt;
  
  
  &lt;center&gt;&lt;strong&gt;Context&lt;/strong&gt;&lt;/center&gt;
&lt;/h2&gt;

&lt;p&gt;Every once in a while I have to stand up a new backend/DB. I used to just use Supabase, but I don't like the idea of not owning my data, and I like my bill being the same number every month.&lt;/p&gt;

&lt;p&gt;I personally &lt;strong&gt;love&lt;/strong&gt; Pocketbase for this. It's ✨one binary✨ with a database (SQLite), auth, file storage, a REST API and an admin dashboard baked in. No install wizard, no 14 microservices, no YAML cathedral. It's what I reach for whenever a project needs a small, self-contained backend. If you want to compare other options, here are some &lt;a href="https://alternativeto.net/software/pocketbase/" rel="noopener noreferrer"&gt;other alternatives&lt;/a&gt; you could check out.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;center&gt;&lt;strong&gt;Challenge&lt;/strong&gt;&lt;/center&gt;
&lt;/h2&gt;

&lt;p&gt;I've now set this up for a handful of projects, and every single time I had to rediscover the steps like a goldfish with a keyboard. So this is the cheat sheet.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we're building
&lt;/h3&gt;

&lt;p&gt;By the end, requests will flow like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;client&lt;/code&gt; (any app that calls your backend) → Reverse proxy tool (&lt;code&gt;nginx&lt;/code&gt;) (HTTPS, on a server) → &lt;code&gt;Docker container&lt;/code&gt; (PocketBase) → &lt;code&gt;volume&lt;/code&gt; (your database)&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%2Fspn9x1e6j5thcn7rw5cd.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%2Fspn9x1e6j5thcn7rw5cd.png" alt="Diagram: a client sends HTTPS requests to nginx on the VPS, which forwards them to PocketBase inside a Docker container, with the data stored in a volume. Direct access to the container's port is blocked." width="799" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The game plan (Agile style)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Run PocketBase in a local dev environment&lt;/li&gt;
&lt;li&gt;Put it behind nginx&lt;/li&gt;
&lt;li&gt;Containerize it and keep it behind the local nginx&lt;/li&gt;
&lt;li&gt;Put nginx and the container on a VPS (remote env), with a domain and HTTPS&lt;/li&gt;
&lt;li&gt;Introduce a container registry for versioning our image (in our case GHCR)&lt;/li&gt;
&lt;li&gt;Automate the deploys with GitHub Actions or another CI/CD tool&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What you'll need
&lt;/h3&gt;

&lt;p&gt;Here's the stack I'm using, and what each piece is called in case you want to swap in your own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A backend/database:&lt;/strong&gt; I'll be using PocketBase, an open source backend in a single executable with SQLite built in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A container platform:&lt;/strong&gt; I'm using Docker (and Docker Compose) because it's what I like to use the most&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A reverse proxy:&lt;/strong&gt; it routes incoming requests to the right app, handles HTTPS, and enforces limits. I'll be using nginx&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version control:&lt;/strong&gt; Git, hosted on GitHub, because I like GitHub&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A container registry:&lt;/strong&gt; a place to store our built Docker images. I use GHCR (GitHub Container Registry)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A CI/CD tool:&lt;/strong&gt; continuous integration/continuous delivery, the thing that builds and deploys for you on every push. I use GitHub Actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A remote environment:&lt;/strong&gt; a VPS (virtual private server) to run it all on. I love DigitalOcean droplets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A domain/subdomain&lt;/strong&gt; for making the DB accessible on the internet&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Optional&lt;/em&gt; &lt;strong&gt;A health check monitor:&lt;/strong&gt; a service that keeps pinging PocketBase's health endpoint and alerts you (or triggers a rollback) when it stops responding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and last but not least, a handy dandy terminal with basic knowledge of SSH.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;BONUS: &lt;a href="https://youtu.be/D4AsrfxgWGQ" rel="noopener noreferrer"&gt;some music&lt;/a&gt; to help you lock in and time yourself. If you don't finish this tutorial within an hour, I'll personally refund you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything in angle brackets like &lt;code&gt;&amp;lt;username&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;website-url&amp;gt;&lt;/code&gt; is a placeholder for your own value.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;1. PocketBase, locally&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Nothing fancy yet. We want PocketBase running on your machine so you know what we're about to ship. Here's how:&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;# grab the binary for your OS/arch from https://github.com/pocketbase/pocketbase/releases&lt;/span&gt;
&lt;span class="c"&gt;# (put it in a `pocketbase/` folder in your project)&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;pocketbase

&lt;span class="c"&gt;# run the migrations, then start the server&lt;/span&gt;
&lt;span class="nv"&gt;PB_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev ./pocketbase migrate up
&lt;span class="nv"&gt;PB_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev ./pocketbase serve

&lt;span class="c"&gt;# create yourself a superuser&lt;/span&gt;
./pocketbase superuser upsert dev-admin@example.com change-me-locally
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're on Windows that's &lt;code&gt;pocketbase.exe&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The admin dashboard lives at &lt;code&gt;http://127.0.0.1:8090/_/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You don't actually &lt;em&gt;need&lt;/em&gt; &lt;code&gt;PB_ENV&lt;/code&gt; (it isn't a PocketBase thing, just an environment variable I made up) or &lt;code&gt;cross-env&lt;/code&gt; (a tiny tool that makes &lt;code&gt;PB_ENV=dev&lt;/code&gt; work in any shell, Windows included), but they're useful for one very specific thing. Locally I want a user to log in with so I'm not creating one by hand every time I wipe my database. But I &lt;strong&gt;definitely&lt;/strong&gt; don't want that user existing in production. So let's make a migration that only runs when &lt;code&gt;PB_ENV=dev&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;./pocketbase migrate create dev_user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// pb_migrations/&amp;lt;timestamp&amp;gt;_dev_user.js&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;reference path="../pb_data/types.d.ts" /&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;migrate&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// bail out unless we are in dev&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;$os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PB_ENV&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dev&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;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;existing&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;DEV_EMAIL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dev@example.com&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;DEV_PASS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change-me-locally&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&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;findFirstRecordByData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DEV_EMAIL&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;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&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="nx"&gt;existing&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;record&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;Record&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;findCollectionByNameOrId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DEV_EMAIL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DEV_PASS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&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;app&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="c1"&gt;// same guard on the way down&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;$os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PB_ENV&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dev&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;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;existing&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="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&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;findFirstRecordByData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dev@example.com&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&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="nx"&gt;existing&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="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&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;That &lt;code&gt;PB_ENV&lt;/code&gt; check is the whole trick. In production the migration runs, shrugs, and does nothing.&lt;/p&gt;

&lt;p&gt;Here's what we've achieved so far:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ PocketBase running locally&lt;/li&gt;
&lt;li&gt;✅ a superuser and a dev-only user&lt;/li&gt;
&lt;li&gt;✅ migrations that know which environment they're in&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. Put it behind nginx&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Right now you're talking to PocketBase directly on port 8090. That's fine on your laptop, but it's not what you want facing the internet. Before we touch the server, let's get used to the setup we'll use there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is nginx doing here?&lt;/strong&gt; It's a reverse proxy. It sits in front of PocketBase, accepts the incoming requests, and forwards them to it. That gives us a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS.&lt;/strong&gt; nginx handles the TLS certificate so PocketBase doesn't have to&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One front door for many apps.&lt;/strong&gt; Several apps can share one server and one port 443, routed by domain name&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guard rails.&lt;/strong&gt; Rate limits and a max upload size, before a request ever reaches PocketBase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; The app itself only listens on localhost and can't be reached directly from the internet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What else could you use?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://caddyserver.com/" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt; is simpler, and gets and renews HTTPS certificates automatically&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://traefik.io/" rel="noopener noreferrer"&gt;Traefik&lt;/a&gt; is built for containers and configures itself from Docker labels&lt;/li&gt;
&lt;li&gt;PocketBase can also serve HTTPS by itself if you hand it a domain, which is the least moving parts if it's the only thing on the box&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why nginx then?&lt;/strong&gt; For me it's familiarity and it gives me everything I need at the moment out of the box; Rate limiting and upload limits in one config file. If you're starting from scratch with a single app, Caddy is a perfectly good answer as well.&lt;/p&gt;

&lt;p&gt;Install nginx (&lt;code&gt;brew install nginx&lt;/code&gt;, &lt;code&gt;sudo apt install nginx&lt;/code&gt;, or the zip from &lt;a href="https://nginx.org/en/download.html" rel="noopener noreferrer"&gt;nginx.org&lt;/a&gt; on Windows) and add this server block to your config. Where it goes depends on your OS: &lt;code&gt;conf.d/&lt;/code&gt;, &lt;code&gt;sites-enabled/&lt;/code&gt;, or inside &lt;code&gt;http {}&lt;/code&gt; in &lt;code&gt;nginx.conf&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# local dev: nginx listens on 8080 and forwards to PocketBase on 8090
&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt; {
    &lt;span class="n"&gt;listen&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;;
    &lt;span class="n"&gt;server_name&lt;/span&gt; &lt;span class="n"&gt;localhost&lt;/span&gt;;

    &lt;span class="c"&gt;# max upload size, i.e. maxSelect:20 * maxSize:10MB per request
&lt;/span&gt;    &lt;span class="n"&gt;client_max_body_size&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;;

    &lt;span class="n"&gt;proxy_set_header&lt;/span&gt; &lt;span class="n"&gt;Host&lt;/span&gt; $&lt;span class="n"&gt;host&lt;/span&gt;;
    &lt;span class="n"&gt;proxy_set_header&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;-&lt;span class="n"&gt;Real&lt;/span&gt;-&lt;span class="n"&gt;IP&lt;/span&gt; $&lt;span class="n"&gt;remote_addr&lt;/span&gt;;
    &lt;span class="n"&gt;proxy_set_header&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;-&lt;span class="n"&gt;Forwarded&lt;/span&gt;-&lt;span class="n"&gt;For&lt;/span&gt; $&lt;span class="n"&gt;proxy_add_x_forwarded_for&lt;/span&gt;;
    &lt;span class="n"&gt;proxy_set_header&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;-&lt;span class="n"&gt;Forwarded&lt;/span&gt;-&lt;span class="n"&gt;Proto&lt;/span&gt; $&lt;span class="n"&gt;scheme&lt;/span&gt;;

    &lt;span class="n"&gt;location&lt;/span&gt; / {
        &lt;span class="n"&gt;proxy_pass&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;://&lt;span class="m"&gt;127&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;:&lt;span class="m"&gt;8090&lt;/span&gt;;
    }
}
&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;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;          &lt;span class="c"&gt;# check the config&lt;/span&gt;
nginx &lt;span class="nt"&gt;-s&lt;/span&gt; reload   &lt;span class="c"&gt;# (or start nginx if it isn't running yet)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;http://localhost:8080/_/&lt;/code&gt; shows the same dashboard, but through nginx. If you plan to use PocketBase's realtime subscriptions, double check &lt;a href="https://pocketbase.io/docs/" rel="noopener noreferrer"&gt;PocketBase's docs&lt;/a&gt; for the extra nginx proxy settings they recommend.&lt;/p&gt;

&lt;p&gt;Later we'll add a rate limit for any collection that strangers are allowed to write to. For now, one thing to take away: nginx doesn't care what's behind it, as long as something answers on the port you give it. Which brings us to the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. Put PocketBase in Docker Compose, behind the same nginx&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Now we put PocketBase in a box, a container, 🐳&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does Docker give us?&lt;/strong&gt; It's a container platform: it packages an app and everything it needs into an image, and runs that image as an isolated container. That makes it easy to set up the same environment across platforms/machines with a single command. To update you just pull the latest image, rollback is the same with an older image, and your data stays safe in a volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What else could you use?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run the binary directly under &lt;code&gt;systemd&lt;/code&gt;. Fewer layers, totally valid for a hobby project, but you're updating and wiring it up by hand every time&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://podman.io/" rel="noopener noreferrer"&gt;Podman&lt;/a&gt;, a Docker-compatible alternative that runs without a background daemon&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&gt; (or a lighter orchestrator like Docker Swarm or Nomad). They run and scale containers across many servers, which is way more than one small PocketBase on one box needs, but good to know they exist for when you outgrow this&lt;/li&gt;
&lt;li&gt;A managed PocketBase host, or a PaaS with persistent disks, if you'd rather not run a server at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the Dockerfile. Notice we only copy in &lt;code&gt;pb_hooks&lt;/code&gt; and &lt;code&gt;pb_migrations&lt;/code&gt;, the code, &lt;strong&gt;not&lt;/strong&gt; the data. Your actual database lives in a volume.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# pocketbase/Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; alpine:3.20&lt;/span&gt;

&lt;span class="c"&gt;# pin whatever version you tested against, bump it on purpose&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; PB_VERSION=0.40.2&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; unzip ca-certificates curl

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /pb&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; pb.zip https://github.com/pocketbase/pocketbase/releases/download/v&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PB_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/pocketbase_&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PB_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;_linux_amd64.zip &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; unzip pb.zip &lt;span class="nt"&gt;-d&lt;/span&gt; /pb/ &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm &lt;/span&gt;pb.zip &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /pb/pocketbase

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pb_hooks /pb/pb_hooks&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pb_migrations /pb/pb_migrations&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8090&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["/pb/pocketbase", "serve", "--http=0.0.0.0:8090"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Heads up, those two &lt;code&gt;COPY&lt;/code&gt; lines will fail the build if the folders don't exist. That's why running &lt;code&gt;migrate up&lt;/code&gt; locally first matters, it makes sure &lt;code&gt;pb_migrations/&lt;/code&gt; is there so the image can actually be built.&lt;/p&gt;

&lt;p&gt;And the compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pocketbase/docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pocketbase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:8091:8090"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PB_ENV=${PB_ENV:-production}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pb_data:/pb/pb_data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pb_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few small details doing big work here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;127.0.0.1:8091:8090&lt;/code&gt; reads as &lt;code&gt;host-ip:host-port:container-port&lt;/code&gt;, and the two port numbers are different on purpose so you can see which is which:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;8090&lt;/strong&gt; is the port PocketBase listens on &lt;em&gt;inside&lt;/em&gt; the container. It's fixed by the Dockerfile and nobody outside the container can see it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8091&lt;/strong&gt; is the port Docker listens on on the machine itself (the host). It passes whatever arrives there into the container's 8090. This is the port nginx forwards to, that's the &lt;code&gt;proxy_pass&lt;/code&gt;. The public internet never calls 8091, it talks to nginx&lt;/li&gt;
&lt;li&gt;8090 is the default and you could have both of them ast 8090 or use any free port.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;That &lt;code&gt;127.0.0.1&lt;/code&gt; (instead of &lt;code&gt;0.0.0.0&lt;/code&gt;) means the container is only reachable through nginx and not directly from the internet
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PB_ENV=${PB_ENV:-production}&lt;/code&gt; defaults to production, so you have to ask for dev on purpose. Our dev-only migration stays asleep unless you say otherwise&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;pb_data&lt;/code&gt; volume, so recreating the container doesn't delete your database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can stop the binary from step 1 now (Ctrl+C). Start the container in dev mode, then point nginx at the new port by changing &lt;code&gt;proxy_pass&lt;/code&gt; to &lt;code&gt;http://127.0.0.1:8091&lt;/code&gt; and reloading:&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;pocketbase
&lt;span class="nv"&gt;PB_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;span class="c"&gt;# PowerShell: $env:PB_ENV="dev"; docker compose up -d --build&lt;/span&gt;
nginx &lt;span class="nt"&gt;-s&lt;/span&gt; reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;http://localhost:8080/_/&lt;/code&gt; works again, same nginx, different thing behind it. The container starts with an empty database, so run the superuser command again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;pocketbase /pb/pocketbase superuser upsert dev-admin@example.com change-me-locally
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what we've achieved so far:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ PocketBase running locally, directly and in a container&lt;/li&gt;
&lt;li&gt;✅ nginx in front of it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;4. Put it on a VPS&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Same two pieces, nginx and the compose file, but this time on a server on the internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a VPS?&lt;/strong&gt; I get a real Linux box with full control, a flat monthly price, and a place for several small apps. PocketBase wants a persistent disk, and a VPS has one.&lt;/p&gt;

&lt;p&gt;Any provider works. A few to look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.digitalocean.com/" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt; (Droplets)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/" rel="noopener noreferrer"&gt;AWS&lt;/a&gt; (Lightsail or EC2)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://azure.microsoft.com/" rel="noopener noreferrer"&gt;Microsoft Azure&lt;/a&gt; (Virtual Machines)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/" rel="noopener noreferrer"&gt;Google Cloud&lt;/a&gt; (Compute Engine)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.hetzner.com/cloud" rel="noopener noreferrer"&gt;Hetzner&lt;/a&gt; (Cloud servers)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the order, and the order matters:&lt;/p&gt;

&lt;h3&gt;
  
  
  DNS first
&lt;/h3&gt;

&lt;p&gt;Add an &lt;strong&gt;A record&lt;/strong&gt; pointing your subdomain (&lt;code&gt;api.yourdomain.com&lt;/code&gt;, or anything you like) at the server's IP. DNS takes a while to propagate and we need it ready for HTTPS, so do it now and do the rest while it settles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prep the server
&lt;/h3&gt;

&lt;p&gt;Log in as root and install what we need (Debian/Ubuntu shown, Docker via the &lt;a href="https://docs.docker.com/engine/install/" rel="noopener noreferrer"&gt;install guide&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;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx certbot python3-certbot-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create a dedicated user for deploys, don't deploy as root:&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;adduser &amp;lt;username&amp;gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /opt/&amp;lt;&lt;span class="nb"&gt;dirname&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &amp;lt;username&amp;gt;:&amp;lt;username&amp;gt; /opt/&amp;lt;&lt;span class="nb"&gt;dirname&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;# the user needs docker access&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &amp;lt;username&amp;gt;
&lt;span class="c"&gt;# somewhere for the deploy key to live&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &amp;lt;username&amp;gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 700 /home/&amp;lt;username&amp;gt;/.ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;strong&gt;not on the server&lt;/strong&gt;, on your own machine, make a deploy key and give the server the public half:&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;# make a dedicated deploy key&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-C&lt;/span&gt; &amp;lt;username&amp;gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/&amp;lt;keyname&amp;gt; &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;passphrase&amp;gt;"&lt;/span&gt;

&lt;span class="c"&gt;# log in as root once and copy the public key over&lt;/span&gt;
ssh root@&amp;lt;vps-ip&amp;gt; &lt;span class="s2"&gt;"cat &amp;gt;&amp;gt; /home/&amp;lt;username&amp;gt;/.ssh/authorized_keys"&lt;/span&gt; &amp;lt; ~/.ssh/&amp;lt;keyname&amp;gt;.pub

&lt;span class="c"&gt;# or, if you want to prepend restrictions to the key&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^/no-port-forwarding,no-agent-forwarding /'&lt;/span&gt; ~/.ssh/&amp;lt;keyname&amp;gt;.pub | ssh root@&amp;lt;vps-ip&amp;gt; &lt;span class="s2"&gt;"cat &amp;gt;&amp;gt; /home/&amp;lt;username&amp;gt;/.ssh/authorized_keys"&lt;/span&gt;

&lt;span class="c"&gt;# make sure the new user can actually log in&lt;/span&gt;
ssh &amp;lt;username&amp;gt;@&amp;lt;vps-ip&amp;gt; &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/&amp;lt;keyname&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A dedicated deploy key is cleaner than reusing your personal one, if it leaks you revoke that one key and nothing else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get the files there and start it
&lt;/h3&gt;

&lt;p&gt;For now we'll build the image right on the server. Run this from your &lt;code&gt;pocketbase/&lt;/code&gt; folder (we're sending the code, not your local data):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scp &lt;span class="nt"&gt;-r&lt;/span&gt; Dockerfile docker-compose.yml pb_hooks pb_migrations &amp;lt;username&amp;gt;@&amp;lt;vps-ip&amp;gt;:/opt/&amp;lt;&lt;span class="nb"&gt;dirname&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/
ssh &amp;lt;username&amp;gt;@&amp;lt;vps-ip&amp;gt; &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/&amp;lt;keyname&amp;gt; &lt;span class="s2"&gt;"cd /opt/&amp;lt;dirname&amp;gt; &amp;amp;&amp;amp; docker compose up -d --build"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;PB_ENV&lt;/code&gt; this time, so it defaults to &lt;code&gt;production&lt;/code&gt; and the dev user is never created.&lt;/p&gt;

&lt;h3&gt;
  
  
  The server's nginx
&lt;/h3&gt;

&lt;p&gt;Same idea as before, with three upgrades: your real domain, the production port, and a rate limit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# throttle public write endpoints to curb spam
&lt;/span&gt;&lt;span class="n"&gt;limit_req_zone&lt;/span&gt; $&lt;span class="n"&gt;binary_remote_addr&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt;=&amp;lt;&lt;span class="n"&gt;zone&lt;/span&gt;-&lt;span class="n"&gt;name&lt;/span&gt;&amp;gt;:&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;=&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;/&lt;span class="n"&gt;m&lt;/span&gt;;

&lt;span class="n"&gt;server&lt;/span&gt; {
    &lt;span class="n"&gt;listen&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;;
    &lt;span class="n"&gt;server_name&lt;/span&gt; &amp;lt;&lt;span class="n"&gt;website&lt;/span&gt;-&lt;span class="n"&gt;url&lt;/span&gt;&amp;gt;;

    &lt;span class="c"&gt;# max upload size, i.e. maxSelect:20 * maxSize:10MB per request
&lt;/span&gt;    &lt;span class="n"&gt;client_max_body_size&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;;

    &lt;span class="n"&gt;proxy_set_header&lt;/span&gt; &lt;span class="n"&gt;Host&lt;/span&gt; $&lt;span class="n"&gt;host&lt;/span&gt;;
    &lt;span class="n"&gt;proxy_set_header&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;-&lt;span class="n"&gt;Real&lt;/span&gt;-&lt;span class="n"&gt;IP&lt;/span&gt; $&lt;span class="n"&gt;remote_addr&lt;/span&gt;;
    &lt;span class="n"&gt;proxy_set_header&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;-&lt;span class="n"&gt;Forwarded&lt;/span&gt;-&lt;span class="n"&gt;For&lt;/span&gt; $&lt;span class="n"&gt;proxy_add_x_forwarded_for&lt;/span&gt;;
    &lt;span class="n"&gt;proxy_set_header&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;-&lt;span class="n"&gt;Forwarded&lt;/span&gt;-&lt;span class="n"&gt;Proto&lt;/span&gt; $&lt;span class="n"&gt;scheme&lt;/span&gt;;

    &lt;span class="c"&gt;# any collection with a public (unauthenticated) createRule
&lt;/span&gt;    &lt;span class="n"&gt;location&lt;/span&gt; = /&lt;span class="n"&gt;api&lt;/span&gt;/&lt;span class="n"&gt;collections&lt;/span&gt;/&amp;lt;&lt;span class="n"&gt;collection&lt;/span&gt;&amp;gt;/&lt;span class="n"&gt;records&lt;/span&gt; {
        &lt;span class="n"&gt;limit_req&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt;=&amp;lt;&lt;span class="n"&gt;zone&lt;/span&gt;-&lt;span class="n"&gt;name&lt;/span&gt;&amp;gt; &lt;span class="n"&gt;burst&lt;/span&gt;=&lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="n"&gt;nodelay&lt;/span&gt;;
        &lt;span class="n"&gt;proxy_pass&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;://&lt;span class="m"&gt;127&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;:&lt;span class="m"&gt;8091&lt;/span&gt;;
    }

    &lt;span class="n"&gt;location&lt;/span&gt; / {
        &lt;span class="n"&gt;proxy_pass&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;://&lt;span class="m"&gt;127&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;:&lt;span class="m"&gt;8091&lt;/span&gt;;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;limit_req&lt;/code&gt; block is for any collection where you let strangers create records, like a contact form. If the rule is public, the internet will find it. Rate limit it.&lt;/p&gt;

&lt;p&gt;Copy it over, enable it, and issue a free HTTPS certificate. Writing to &lt;code&gt;/etc/nginx&lt;/code&gt; and running &lt;code&gt;certbot&lt;/code&gt; needs root, so use your root login here. Your deploy user shouldn't need that power, and I'd keep it that way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scp nginx-&amp;lt;website-url&amp;gt;.conf root@&amp;lt;vps-ip&amp;gt;:/etc/nginx/sites-available/&amp;lt;website-url&amp;gt;
ssh root@&amp;lt;vps-ip&amp;gt; &lt;span class="s2"&gt;"ln -s /etc/nginx/sites-available/&amp;lt;website-url&amp;gt; /etc/nginx/sites-enabled/ &amp;amp;&amp;amp; nginx -t &amp;amp;&amp;amp; systemctl reload nginx"&lt;/span&gt;
&lt;span class="c"&gt;# once DNS has propagated&lt;/span&gt;
ssh root@&amp;lt;vps-ip&amp;gt; &lt;span class="s2"&gt;"certbot --nginx -d &amp;lt;website-url&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Certbot rewrites the site file in place and adds the 443 block plus an 80 → 443 redirect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your production superuser
&lt;/h3&gt;

&lt;p&gt;The dev account was never created here (thanks &lt;code&gt;PB_ENV&lt;/code&gt;), so nobody can log in yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &amp;lt;username&amp;gt;@&amp;lt;vps-ip&amp;gt; &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/&amp;lt;keyname&amp;gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/&amp;lt;&lt;span class="nb"&gt;dirname&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;# confirm the container is running&lt;/span&gt;
docker compose ps
&lt;span class="c"&gt;# create the real superuser with a real password&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;pocketbase /pb/pocketbase superuser upsert &amp;lt;superuser-email&amp;gt; &amp;lt;strong-password&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;https://&amp;lt;website-url&amp;gt;/_/&lt;/code&gt; and log in.&lt;/p&gt;

&lt;p&gt;Here's what we've achieved so far:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ PocketBase running on a server, in a container, only reachable through nginx&lt;/li&gt;
&lt;li&gt;✅ a deploy user with its own key&lt;/li&gt;
&lt;li&gt;✅ a real superuser and no dev account in production&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;5. A container registry&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Right now the server builds the image itself. It works, but every deploy means sending files over and building on a small machine, and there's no clean record of which version is running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's a container registry?&lt;/strong&gt; It's a place to store built Docker images, with tags. Instead of building on the server, we build once somewhere else, push the image to the registry with a version tag, and the server just &lt;strong&gt;pulls&lt;/strong&gt; that exact image. That gets us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Faster, lighter deploys.&lt;/strong&gt; The server only downloads, it never builds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioning.&lt;/strong&gt; We tag each image with the git commit that produced it, so you always know what's running and can roll back by pulling an older tag
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same image everywhere.&lt;/strong&gt; What you tested is what you deploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What else could you use?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker Hub&lt;/strong&gt;, the default and the most common, but anonymous pulls are rate limited&lt;/li&gt;
&lt;li&gt;A cloud provider's own registry (AWS ECR, DigitalOcean Container Registry, and so on), good if you already live there, but another service to set up and pay attention to&lt;/li&gt;
&lt;li&gt;Running your own registry, which is one more server to look after&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why GHCR?&lt;/strong&gt; &lt;a href="https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry" rel="noopener noreferrer"&gt;GitHub Container Registry&lt;/a&gt; (&lt;code&gt;ghcr.io&lt;/code&gt;) lives right next to the code. Our deploy will run on GitHub Actions anyway, and Actions can log in to GHCR with the token it already has, no extra account and no extra secret to manage.&lt;/p&gt;

&lt;p&gt;Your compose file changes in one place: swap &lt;code&gt;build: .&lt;/code&gt; for an &lt;code&gt;image:&lt;/code&gt; line. This is the file that lives on the server from now on:&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="c1"&gt;# pocketbase/docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pocketbase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/&amp;lt;github-user&amp;gt;/&amp;lt;dirname&amp;gt;:${IMAGE_TAG:-latest}&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:8091:8090"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PB_ENV=production&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pb_data:/pb/pb_data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pb_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two notes. Image names on GHCR have to be lowercase. And &lt;code&gt;${IMAGE_TAG:-latest}&lt;/code&gt; means "use this tag, or &lt;code&gt;latest&lt;/code&gt; if I didn't give one", which is how a deploy picks a specific version.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;6. Automate it with GitHub Actions&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;You could build and push by hand, but we've built all this so we can just push to &lt;code&gt;main&lt;/code&gt; and let the computer handle it automatically (AI, ladies and gentlemen).&lt;/p&gt;

&lt;p&gt;Here's the whole pipeline at a glance:&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%2Fb28w5orsrwrtbfmn53rm.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%2Fb28w5orsrwrtbfmn53rm.png" alt="Diagram: pushing to main triggers GitHub Actions, which builds the image and pushes it to GHCR. The VPS pulls the image and runs compose up, then a health check decides whether the deploy passes or fails." width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, give GitHub the key it needs. In your repo settings, add these as &lt;strong&gt;Actions secrets&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;VPS_HOST&lt;/code&gt; = the VPS's IP&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VPS_USER&lt;/code&gt; = &lt;code&gt;&amp;lt;username&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VPS_SSH_KEY&lt;/code&gt; = the &lt;strong&gt;private&lt;/strong&gt; key that matches the authorized key on the VPS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VPS_SSH_PASSPHRASE&lt;/code&gt; = the passphrase, only if you set one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the workflow. It builds the image, pushes it to GHCR, copies the compose file to the server, pulls and restarts, and waits for PocketBase to say it's healthy.&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="c1"&gt;# .github/workflows/deploy-pocketbase.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy PocketBase&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pocketbase/**"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.github/workflows/deploy-pocketbase.yml"&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to GHCR&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.actor }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push image&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./pocketbase&lt;/span&gt;
          &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./pocketbase/Dockerfile&lt;/span&gt;
          &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;ghcr.io/&amp;lt;github-user&amp;gt;/&amp;lt;dirname&amp;gt;:${{ github.sha }}&lt;/span&gt;
            &lt;span class="s"&gt;ghcr.io/&amp;lt;github-user&amp;gt;/&amp;lt;dirname&amp;gt;:latest&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Copy compose file to VPS&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/scp-action@v0.1.7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_SSH_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pocketbase/docker-compose.yml"&lt;/span&gt;
          &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/opt/&amp;lt;dirname&amp;gt;/"&lt;/span&gt;
          &lt;span class="na"&gt;strip_components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to VPS&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v1.0.3&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GHCR_ACTOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.actor }}&lt;/span&gt;
          &lt;span class="na"&gt;GHCR_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;IMAGE_TAG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.sha }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_SSH_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;envs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GHCR_ACTOR,GHCR_TOKEN,IMAGE_TAG&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_ACTOR" --password-stdin&lt;/span&gt;
            &lt;span class="s"&gt;cd /opt/&amp;lt;dirname&amp;gt;&lt;/span&gt;
            &lt;span class="s"&gt;docker compose pull pocketbase&lt;/span&gt;
            &lt;span class="s"&gt;docker compose up -d&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Health check&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v1.0.3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_SSH_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;for i in $(seq 1 30); do&lt;/span&gt;
              &lt;span class="s"&gt;if curl -sf http://127.0.0.1:8091/api/health &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; then&lt;/span&gt;
                &lt;span class="s"&gt;echo "Health check passed"&lt;/span&gt;
                &lt;span class="s"&gt;exit 0&lt;/span&gt;
              &lt;span class="s"&gt;fi&lt;/span&gt;
              &lt;span class="s"&gt;echo "Attempt $i/30 - waiting..."&lt;/span&gt;
              &lt;span class="s"&gt;sleep 2&lt;/span&gt;
            &lt;span class="s"&gt;done&lt;/span&gt;
            &lt;span class="s"&gt;echo "Health check failed after 30 attempts"&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because of &lt;code&gt;workflow_dispatch&lt;/code&gt;, you can also trigger it by hand from the Actions tab, no merging required. Very handy for the "I changed nothing, please just try again" moments.&lt;/p&gt;

&lt;p&gt;Push to &lt;code&gt;main&lt;/code&gt; and watch the Actions tab do its thing. Every deploy is now tagged with its commit, and that's your rollback button.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Finishing touches&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Point your frontend at it. For me that's an environment variable in Vercel:&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;NEXT_PUBLIC_POCKETBASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://&amp;lt;website-url&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redeploy the frontend, and that's it.&lt;/p&gt;

&lt;p&gt;For the days when something is off, here's my "turn it off and on again" toolkit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &amp;lt;username&amp;gt;@&amp;lt;vps-ip&amp;gt; &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/&amp;lt;keyname&amp;gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/&amp;lt;&lt;span class="nb"&gt;dirname&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
docker compose ps                          &lt;span class="c"&gt;# status&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;                     &lt;span class="c"&gt;# tail the logs&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;                       &lt;span class="c"&gt;# start/restart (does NOT pull a new image)&lt;/span&gt;
docker compose pull &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;   &lt;span class="c"&gt;# get the latest image, then restart&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;pocketbase sh          &lt;span class="c"&gt;# shell into the running container&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I left out, because it's beyond a one hour setup but you'll want it soon: &lt;strong&gt;backups&lt;/strong&gt; of the &lt;code&gt;pb_data&lt;/code&gt; volume, a &lt;strong&gt;firewall&lt;/strong&gt; that only allows SSH, HTTP and HTTPS, and some kind of &lt;strong&gt;uptime monitoring&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's the whole journey:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ PocketBase running locally with a dev-only user&lt;/li&gt;
&lt;li&gt;✅ nginx in front of it, and why&lt;/li&gt;
&lt;li&gt;✅ a container that ships your code and keeps your data in a volume&lt;/li&gt;
&lt;li&gt;✅ a VPS with a domain, HTTPS and rate limiting&lt;/li&gt;
&lt;li&gt;✅ a registry so every deploy has a version&lt;/li&gt;
&lt;li&gt;✅ a pipeline that deploys on every push to &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And there you have it! This was how to deploy your own PocketBase backend to a remote environment, in less than an hour (or something like that).&lt;/p&gt;

&lt;h6&gt;P.S. if you only need this for a hobby project, you could skip the pipeline and the registry and stop at step 4. It works fine, you'll just be doing the deploys by hand.&lt;/h6&gt;

&lt;h6&gt;If you have any comments, questions, or concerns, please keep them to yourself.&lt;/h6&gt;

&lt;h6&gt;All typos are on purpose.&lt;/h6&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pocketbase.io/docs/" rel="noopener noreferrer"&gt;PocketBase Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://claude.ai/" rel="noopener noreferrer"&gt;Unpaid Intern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://youtu.be/D4AsrfxgWGQ" rel="noopener noreferrer"&gt;Background Music&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adelbeit.com/" rel="noopener noreferrer"&gt;Shameless Plug&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>fullstack</category>
      <category>database</category>
      <category>vps</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Choosing a video hosting provider for hobby projects</title>
      <dc:creator>Adele Beitvashahi</dc:creator>
      <pubDate>Mon, 30 Mar 2026 02:06:36 +0000</pubDate>
      <link>https://dev.to/adelbeit/choosing-a-video-hosting-provider-for-hobby-projects-12be</link>
      <guid>https://dev.to/adelbeit/choosing-a-video-hosting-provider-for-hobby-projects-12be</guid>
      <description>&lt;p&gt;Something embarrassing happened to me recently. I was at a networking event and I was excited to show someone my &lt;a href="https://adelbeit.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; and passion project &lt;a href="https://jam.adelbeit.com" rel="noopener noreferrer"&gt;Jamroom&lt;/a&gt;. As soon as I pulled them up, I remembered how long it'd been since I created these projects. The UI/UX I'd built a couple of years ago felt stale, and there were unpolished edges I never went back to. &lt;/p&gt;

&lt;p&gt;So I spent last weekend to revamp the UI, rethink some of the UX, and I even finally recorded a demo for Jamroom&lt;sup id="fnref1"&gt;1&lt;/sup&gt; to showcase on my website!&lt;/p&gt;

&lt;p&gt;A new challenge I was presented with then was where to host the demo video. In the past I'd always reach for the quick and dirty solutions like hosting on &lt;a href="https://vimeo.com" rel="noopener noreferrer"&gt;Vimeo&lt;/a&gt;, &lt;a href="https://youtube.com" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt;'s unlisted videos, or &lt;a href="https://github.com" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; even. This time I decided it was time for change, I wanted to host my assets on my own infra. To do this I came up with two requirements:&lt;br&gt;
1) The solution needs to be free/dirt cheap&lt;br&gt;
2) The setup can't take more than a few minutes, time is money!&lt;/p&gt;

&lt;p&gt;Did some research and landed on &lt;a href="https://developers.cloudflare.com/r2/" rel="noopener noreferrer"&gt;Cloudflare R2&lt;/a&gt;, they offer a generous free tier. But mid-onboarding I realized in order to use the free tier, I'd have to either:&lt;br&gt;
1) Switch my DNS provider to &lt;a href="https://www.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt;, having to migrate all my DNS records, which would absolutely take more than a few minutes.&lt;br&gt;
2) Setup URL pre-signing on my backend to provide public access to the videos in my private bucket. Which would be unnecessary work for my needs at this time.&lt;/p&gt;

&lt;p&gt;So I kept digging and voila! Enter &lt;a href="https://vercel.com/docs/storage/vercel-blob" rel="noopener noreferrer"&gt;Vercel Blob&lt;/a&gt; storage, their free tier offers free public access to your assets, albeit it's very limited. But it's good enough for now! There'll come a time when I'll have to finally setup the URL pre-signing to take advantage of the more generous free services out there, but till then I can invest my time in more pressing projects. &lt;/p&gt;

&lt;p&gt;Here are my realistic options, optimizing for ease of setup/use and cost.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;provider&lt;/th&gt;
&lt;th&gt;cheapest tier&lt;/th&gt;
&lt;th&gt;tier limits&lt;/th&gt;
&lt;th&gt;public URL access&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.backblaze.com/b2/cloud-storage.html" rel="noopener noreferrer"&gt;Backblaze B2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;forever free&lt;/td&gt;
&lt;td&gt;10GB storage, unlimited free egress, 1M writes + 10M reads/mo&lt;/td&gt;
&lt;td&gt;Yes—public buckets support direct URLs , but cannot map custom domain without Cloudflare workaround&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://supabase.com/docs/guides/storage" rel="noopener noreferrer"&gt;Supabase Storage&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;free forever&lt;/td&gt;
&lt;td&gt;1GB file storage, 5GB bandwidth, 50K MAUs&lt;/td&gt;
&lt;td&gt;Yes—public buckets provide direct asset URLs without auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.digitalocean.com/products/spaces" rel="noopener noreferrer"&gt;DigitalOcean Spaces&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;$5/month&lt;/td&gt;
&lt;td&gt;250GB storage, 1TB outbound transfer&lt;/td&gt;
&lt;td&gt;Yes—files can be set to public permissions with direct URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://vercel.com/docs/storage/vercel-blob" rel="noopener noreferrer"&gt;Vercel Blob&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;free (Hobby)&lt;/td&gt;
&lt;td&gt;1GB storage, 10GB Blob Data Transfer/month&lt;/td&gt;
&lt;td&gt;Yes—native public access with permanent URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://aws.amazon.com/s3/" rel="noopener noreferrer"&gt;AWS S3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;free 12mo&lt;/td&gt;
&lt;td&gt;5GB storage, 20K GET + 2K PUT requests/month, 100GB data transfer out&lt;/td&gt;
&lt;td&gt;Yes via public bucket or presigned URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Links
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://68fwap572n8u3jpb.public.blob.vercel-storage.com/jamroom-demo/jamroom-demo.mp4" rel="noopener noreferrer"&gt;Demo video&lt;/a&gt;&lt;br&gt;
&lt;a href="https://jam.adelbeit.com" rel="noopener noreferrer"&gt;Jamroom Website&lt;/a&gt;&lt;br&gt;
&lt;a href="https://adelbeit.com" rel="noopener noreferrer"&gt;My Website&lt;/a&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Jamroom is a passion project I built a couple of years ago to teach myself about real time messaging systems and experiment with music. You can join a room with your friends and play virtual instruments (keyboard and drums) together in real time.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>devdiaries</category>
      <category>vercel</category>
      <category>cloudfront</category>
    </item>
    <item>
      <title>AI Resume Filters Can't Read Between the Lines</title>
      <dc:creator>Adele Beitvashahi</dc:creator>
      <pubDate>Wed, 18 Mar 2026 21:33:29 +0000</pubDate>
      <link>https://dev.to/adelbeit/ai-resume-filters-cant-read-between-the-lines-56lo</link>
      <guid>https://dev.to/adelbeit/ai-resume-filters-cant-read-between-the-lines-56lo</guid>
      <description>&lt;p&gt;Picture this: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A Resume bullet says: "built X system that served 1M+ users at "&lt;br&gt;
A job description says: "implement metrics, logging, and dashboards."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI sees no overlap and disqualifies the candidate. But any engineer who shipped to a million users (heck anyone who has built anything for any business) has &lt;em&gt;absolutely&lt;/em&gt; built observability infrastructure, they just didn't waste resume space saying so. Sometimes they handcrafted the dashboards, sometimes they used already present tooling like &lt;a href="https://mixpanel.com/" rel="noopener noreferrer"&gt;Mixpanel&lt;/a&gt; and other observability platforms with built in dashboarding capabilities. The point is &lt;strong&gt;businesses require observability period&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Observability is just one example. Testing, code review, documentation, incident response, none of it makes the resume because those are all implementation details of the role, the meat and the potatoes are the ✨results✨ &lt;/p&gt;

&lt;p&gt;Of course that's not to say those implementation details are unimportant, but time and place for everything, they're meant to be communicated verbally &lt;strong&gt;during the interview&lt;/strong&gt;. That's what interviews are for.&lt;/p&gt;

&lt;p&gt;Resumes are meant to showcase highlights of your career through story telling focused on impactful work, not serve as a laundry list of every single task you've done. That'd turn it into an unsustainably large alphabet soup. A soup so large even the AI systems ingesting it would lose their appetite.&lt;/p&gt;

&lt;p&gt;Of course I don't believe we'll ever give up AI in recruiting or any other industry. The AI isn't the problem. The prompt behind it is, and that's on whoever built the filtering tool. A productive step may be to work towards more intelligent system prompts that do more than just keyword matching. That's what LLMs are for after all. Here's an example of a system prompt designed to infer implied skills from context clues. Things like treating "served 1M+ users" as an observability signal, or treating quantified improvements as evidence of instrumentation.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: this is illustrative, not a comprehensive solution.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a resume intelligence system. Your job is to evaluate a candidate's 
true qualifications by reading between the lines, not just scanning for keywords.

Experienced engineers don't list every task they performed. They tell a story 
of impact. Your job is to understand what that story implies.

When evaluating a resume against a job description, consider what the candidate 
almost certainly did but didn't write down. A system serving millions of users 
implies observability. Quantified improvements imply instrumentation. Enterprise 
experience implies process. Real-time features imply distributed systems thinking.
Startup experience implies ownership and autonomy.

Use context clues from company size, scale, tech stack, and career trajectory 
to infer unstated but implied competencies. Distinguish between a real skill gap 
and a resume brevity gap. Flag the latter for interview validation rather than 
automatic disqualification.

The best resumes are highlight reels, not task logs. They're narrative-driven 
stories of impact, not exhaustive records of everything someone has ever done. 
Don't penalize candidates for writing a good one.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>recruiting</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to use Filters in PrimeReact DataTables</title>
      <dc:creator>Adele Beitvashahi</dc:creator>
      <pubDate>Mon, 09 Sep 2024 20:07:34 +0000</pubDate>
      <link>https://dev.to/adelbeit/how-to-use-filters-in-primereact-datatables-mhp</link>
      <guid>https://dev.to/adelbeit/how-to-use-filters-in-primereact-datatables-mhp</guid>
      <description>&lt;p&gt;[UPDATE]: I've made a comprehensive &lt;a href="https://stackblitz.com/edit/vz6wt2-cs8nz4?file=src%2FApp.tsx" rel="noopener noreferrer"&gt;demo on blitzstack&lt;/a&gt; which goes in depth on the different filtering scenarios that are neglected in the primereact docs. I'll update this article into a full writeup when I get the chance but for now that should serve all your filtering needs and then some! check it out&lt;/p&gt;

&lt;p&gt;[EDIT]: &lt;a href="https://github.com/primefaces/primereact/issues/3325#:~:text=To%20apply%20a%20custom%20filter%2C%20the%20following%20conditions%20must%20be%20met." rel="noopener noreferrer"&gt;insightful github discussion&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I wrote a very long post then decided an image is worth a thousand words, so there you go. &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%2Fyaotv5dhsmk0329610ao.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%2Fyaotv5dhsmk0329610ao.png" alt="Code diagram" width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Although the images uploaded to dev.to get downscaled so maybe this one is only worth 250 words.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The aim of this post was to reduce headaches induced by things unsaid in the &lt;a href="https://v9.primereact.org/datatable/#filter" rel="noopener noreferrer"&gt;official docs&lt;/a&gt;. There was a lack of resources online for using filters with MultiSelect and Dropdown components correctly with a more-than-basic data structure setup. So I'm using this post as a resource for future me and anyone else that might find this useful&lt;/p&gt;

&lt;p&gt;If you look at the top left of that image there is a string[] that holds fruit names, that's because you can also use string[] instead of Object[] for the filter item list. I didn't expand on it because the &lt;a href="https://v9.primereact.org/datatable/#filter" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; do a good enough job doing it. &lt;/p&gt;

&lt;p&gt;I tried to cover things the docs failed to cover.&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to setup dev containers in windows</title>
      <dc:creator>Adele Beitvashahi</dc:creator>
      <pubDate>Wed, 28 Aug 2024 02:41:52 +0000</pubDate>
      <link>https://dev.to/adelbeit/how-to-setup-dev-containers-in-windows-3fcn</link>
      <guid>https://dev.to/adelbeit/how-to-setup-dev-containers-in-windows-3fcn</guid>
      <description>&lt;p&gt;If you are going to work with dev containers in Windows you need to realize that they will break HMR (hot module reload) and increase build time unless you put your code inside WSL instead of the windows filesystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Dev Containers Work in Windows
&lt;/h2&gt;

&lt;p&gt;Dev containers use Docker, which relies on WSL. When you spin up a dev container, VSCode starts a docker container in WSL, and mounts the files from windows (if they are stored there). This means changes have to cross the WSL-Windows environment. This tends to break HMR, because HMR relies on file change API of the host env, and the filesystem events don't travel well between different host environments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn89s3s05asuhtbwens2d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn89s3s05asuhtbwens2d.png" alt="code stored in Windows vs WSL"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Correct setup for dev containers in Windows
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Dev container setup in WSL&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://code.visualstudio.com/download" rel="noopener noreferrer"&gt;Install Visual Studio Code&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enable WSL 2 by following the &lt;a href="https://learn.microsoft.com/windows/wsl/install" rel="noopener noreferrer"&gt;WSL 2 installation guide&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install Ubuntu (or your preferred Linux distribution) from the &lt;a href="https://www.microsoft.com/p/ubuntu/9nblggh4msv6" rel="noopener noreferrer"&gt;Microsoft store&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.docker.com/docker-for-windows/wsl-tech-preview/#download" rel="noopener noreferrer"&gt;Install Docker Desktop Stable 2.3.0.2&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finally &lt;a href="https://docs.docker.com/desktop/wsl/#download:~:text=When%20Docker%20Desktop%20starts%2C%20go%20to%20Settings%20%3E%20Resources%20%3E%20WSL%20Integration" rel="noopener noreferrer"&gt;enable Docker support in WSL 2 distros&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Follow all the directions &lt;a href="https://code.visualstudio.com/blogs/2020/07/01/containers-wsl" rel="noopener noreferrer"&gt;here&lt;/a&gt; to finish setup. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Clone repo inside the WSL filesystem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now you should be able to do yarn &amp;amp;&amp;amp; yarn dev inside WSL and HMR should work. &lt;/p&gt;

</description>
      <category>devcontainer</category>
      <category>vscode</category>
      <category>react</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Connect to Azure VM w/SSH Through Azure CLI</title>
      <dc:creator>Adele Beitvashahi</dc:creator>
      <pubDate>Mon, 12 Aug 2024 20:24:51 +0000</pubDate>
      <link>https://dev.to/adelbeit/connect-to-azure-vm-wssh-through-azure-cli-2ih8</link>
      <guid>https://dev.to/adelbeit/connect-to-azure-vm-wssh-through-azure-cli-2ih8</guid>
      <description>&lt;h4&gt;
  
  
  1. Create your VM
&lt;/h4&gt;

&lt;h4&gt;
  
  
  2. Assign a public IP to the virtual network/subnet that the VM is under
&lt;/h4&gt;

&lt;h4&gt;
  
  
  3. Install azure cli &amp;amp; login:
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;az login&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Install ssh extension for azure cli
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;az extension add --name ssh&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  5. SSH into vm
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;az ssh vm --private-key-file \path\to\private\key --resource-group &amp;lt;resource group name&amp;gt; --name &amp;lt;vm name&amp;gt; --local-user &amp;lt;local username (default: 'azureuser')&amp;gt;&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  FAQ
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. How to get vm/resource names
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;az vm list -d -o table&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  2. &lt;code&gt;az login&lt;/code&gt; not working on Windows
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;az login --use-device-code&lt;/code&gt;&lt;/p&gt;

</description>
      <category>ssh</category>
      <category>azure</category>
      <category>vm</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to remove AI summary search feature on Google search</title>
      <dc:creator>Adele Beitvashahi</dc:creator>
      <pubDate>Wed, 08 May 2024 21:52:37 +0000</pubDate>
      <link>https://dev.to/adelbeit/how-to-remove-ai-summary-search-feature-on-google-search-560f</link>
      <guid>https://dev.to/adelbeit/how-to-remove-ai-summary-search-feature-on-google-search-560f</guid>
      <description>&lt;h1&gt;
  
  
  You can't turn it off, use DuckDuckGo or another search engine that's independent of large companies.
&lt;/h1&gt;

</description>
      <category>search</category>
      <category>unwantedfeatures</category>
    </item>
    <item>
      <title>How to persist client-side preferences on the client in Svelte (w/o DB)</title>
      <dc:creator>Adele Beitvashahi</dc:creator>
      <pubDate>Sun, 21 Apr 2024 08:48:19 +0000</pubDate>
      <link>https://dev.to/adelbeit/how-to-persist-user-preferences-in-svelte-wo-db-51nf</link>
      <guid>https://dev.to/adelbeit/how-to-persist-user-preferences-in-svelte-wo-db-51nf</guid>
      <description>&lt;h2&gt;
  
  
  &lt;center&gt;&lt;strong&gt;Context&lt;/strong&gt;&lt;/center&gt;
&lt;/h2&gt;

&lt;p&gt;I was in the middle of creating a static site with Svelte. Now of course you might say, why not just make it with plain HTML and CSS. To that I say, it's 2024 and you can go to jail for something like that. &lt;/p&gt;

&lt;p&gt;Now, I'm a React developer by trade, but I chose Svelte for this project, because; 1) it ships less JS to the browser due to it not using a virtual DOM. 2) it's excellent for static site generation (SSG), which is what I needed for this project. and most importantly, 3) it was time I finally learned it and saw what the craze was about, I can't go a day without hearing about the amazing developer experience (dx) Svelte provides. And by golly was I in for a treat! Seriously everyone &lt;strong&gt;needs&lt;/strong&gt; to learn Svelte.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;center&gt;&lt;strong&gt;Challenge&lt;/strong&gt;&lt;/center&gt;
&lt;/h2&gt;

&lt;p&gt;I needed to provide the website content in different locales, and I wanted to persist their preference across all time and space; all pages, routes, and sessions. This challenge is also applicable to website theming, and potentially any other user preference setting that isn't saved on serverside.&lt;/p&gt;

&lt;p&gt;First step is setting and viewing the user preference. &lt;/p&gt;

&lt;p&gt;Here's how:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/+page.svelte&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c1"&gt;// defaults to 'en';&lt;/span&gt;
  &lt;span class="kd"&gt;let&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="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Your&lt;/span&gt; &lt;span class="nx"&gt;preference&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="nx"&gt;to&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="o"&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="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="s2"&gt;en&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;gt;&lt;/span&gt;&lt;span class="nx"&gt;english&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="o"&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="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="s2"&gt;es&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;gt;&lt;/span&gt;&lt;span class="nx"&gt;spanish&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="o"&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="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="s2"&gt;fr&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;gt;&lt;/span&gt;&lt;span class="nx"&gt;french&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is all good and well for a page, but if we wanna share it &lt;code&gt;between pages&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Enter, Svelte &lt;code&gt;+layout.svelte&lt;/code&gt;. If we move that code to the root layout, every descendant of that route (page.svelte and layout.svelte alike) will have that layout display without changing. If I navigate between routes as long as I don't move up above that layout, the preference will stay. &lt;/p&gt;

&lt;p&gt;Now the next issue is accessing that preference from a different component/page. To do that we will need a store. Which is just an object with a subscribe function. Whatever subscribes to that store will update when the store updates.&lt;/p&gt;

&lt;p&gt;Here's a brief review of how to create a &lt;a href="https://svelte.dev/docs/svelte-store" rel="noopener noreferrer"&gt;store in Svelte&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// $lib/utils/stores.js&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;writable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;svelte/store&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;defaultLocale&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;locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;writable&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The locale selector is located inside the navbar, which I want to be nearly global to this site so it will live in &lt;code&gt;+layout.svelte&lt;/code&gt;&lt;br&gt;
Here's how to use that store, in a dumbed down version of my use case.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/+layout.svelte&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;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;locale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$lib/utils/stores.js&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;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Your&lt;/span&gt; &lt;span class="nx"&gt;preference&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="nx"&gt;to&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="o"&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="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="s2"&gt;en&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;gt;&lt;/span&gt;&lt;span class="nx"&gt;english&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="o"&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="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="s2"&gt;es&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;gt;&lt;/span&gt;&lt;span class="nx"&gt;spanish&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="o"&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="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="s2"&gt;fr&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;gt;&lt;/span&gt;&lt;span class="nx"&gt;french&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the state of locale will persist across page/route navigations, but it will reset upon refresh and or closing and reopening the site. &lt;/p&gt;

&lt;p&gt;So naturally we will want to persist the state somewhere tangible, and more enduring.&lt;br&gt;
How about ✨local storage✨? &lt;br&gt;
Let's do that, we can reuse the code from above and subscribe the localStorage to the store. So updates to store also update localStorage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// $lib/utils/stores.js&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;browser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$app/environment&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;writable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;svelte/store&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;defaultLocale&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="c1"&gt;// retrieve localStorage value if it's been set already&lt;/span&gt;
&lt;span class="c1"&gt;// important for persisting through refreshes&lt;/span&gt;
&lt;span class="c1"&gt;// broswer check is needed because localstorage doesn't exist on server side&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;storedLocale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;locale&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;defaultLocale&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;locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;storedLocale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// subscribe to changes&lt;/span&gt;
&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;val&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;browser&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;locale&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/+layout.svelte&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;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;locale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$lib/utils/stores.js&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;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Your&lt;/span&gt; &lt;span class="nx"&gt;preference&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="nx"&gt;to&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="o"&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="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="s2"&gt;en&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;gt;&lt;/span&gt;&lt;span class="nx"&gt;english&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="o"&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="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="s2"&gt;es&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;gt;&lt;/span&gt;&lt;span class="nx"&gt;spanish&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="o"&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="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="s2"&gt;fr&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;gt;&lt;/span&gt;&lt;span class="nx"&gt;french&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what we've achieved so far:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ get &amp;amp; set a preference &lt;/li&gt;
&lt;li&gt;✅ persist it through pages/routes&lt;/li&gt;
&lt;li&gt;✅ persist through refreshes AND sessions (new tab/window)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But what's this?? there's a new problem. Everytime you reload the page, there's a layout shift, where the value of the preference will be set to its initial default before it fetches the stored localstorage value.&lt;/p&gt;

&lt;p&gt;This happens because of &lt;code&gt;server side rendering (SSR)&lt;/code&gt;, which is an amazing amazing feature that's readily available to us today. It works by partially rendering the website on server side before sending it to the client, and then &lt;code&gt;hydrating&lt;/code&gt; it with the rest of the javascript that's necessary for it to fully render, clientside JS basically. I won't go into details of why and how SSR is good, you know how to google.&lt;/p&gt;

&lt;p&gt;The problem is that, our user preferences are stored in localstorage, which is only available on client side. The server that's performing SSR is unaware of this localstorage. So when it partially renders the page, &lt;code&gt;localStorage.getItem()&lt;/code&gt; just returns undefined. So it will default to the fallback value of "en". Even though the users might have specified otherwise in their previous visit, and stored that preference in their localStorage. There are 2 ways to solve this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We can add conditional rendering for any affected content on the site so that it doesn't render until our hydration is fully complete, but that will defeat the purpose of SSR.&lt;/li&gt;
&lt;li&gt;We can use cookies 🍪 to store user preferences. Because unlike localStorage, cookies persist on user's system &lt;strong&gt;and&lt;/strong&gt; the server will have access to them. Perfect!&lt;/li&gt;
&lt;/ol&gt;

&lt;h6&gt;P.S. we could also try and fetch localstorage value first thing in our lifecycle events like onMount, but that doesn't follow the Svelte way. It doesn't fully embrace SSR as we are interrupting the initial load with dataloading/fetching, which should be done on server side as much as possible.

If we were in React land, then it'd be more acceptable.&lt;/h6&gt;

&lt;p&gt;To send and receive the preference cookies, we will need to setup a server js file for our layout that will handle the data fetching and initial data setting through Svelte's &lt;a href="https://kit.svelte.dev/docs/load" rel="noopener noreferrer"&gt;&lt;code&gt;load&lt;/code&gt; function&lt;/a&gt;. That data is then loaded into our route, as defined in +layout.svelte.&lt;/p&gt;

&lt;p&gt;For this, I'll move most of the store logic to +layout.svelte&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// $lib/utils/stores.js&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;writable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;svelte/store&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;locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/+layout.server.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$lib/data/en.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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;cookies&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;defaultLocale&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preferences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cookies&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="s2"&gt;locale&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;defaultLocale&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;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;// routes/+layout.svelte&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;locale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$lib/utils/stores.js&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;browser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$app/environment&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;setCookie&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$lib/utils/cookies.js&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;let&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// use the cookie value&lt;/span&gt;
  &lt;span class="nx"&gt;$locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preferences&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;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;browser&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;storedLocale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;locale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// prefer localstorage value if cookie is unreliable for any reason.&lt;/span&gt;
    &lt;span class="nx"&gt;$locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;storedLocale&lt;/span&gt; &lt;span class="o"&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="c1"&gt;// subscribe localstorage and cookies to the locale store&lt;/span&gt;
  &lt;span class="nl"&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;browser&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;locale&lt;/span&gt;&lt;span class="dl"&gt;"&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;// setCookie is a custom function I wrote for managing cookies easier, you can find it under this codeblock&lt;/span&gt;
    &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;setCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;locale&lt;/span&gt;&lt;span class="dl"&gt;"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Your&lt;/span&gt; &lt;span class="nx"&gt;preference&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="nx"&gt;to&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="o"&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="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="s2"&gt;en&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;gt;&lt;/span&gt;&lt;span class="nx"&gt;english&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="o"&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="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="s2"&gt;es&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;gt;&lt;/span&gt;&lt;span class="nx"&gt;spanish&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="o"&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="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="s2"&gt;fr&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;gt;&lt;/span&gt;&lt;span class="nx"&gt;french&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// $lib/utils/cookies.js&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;browser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$app/environment&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;function&lt;/span&gt; &lt;span class="nf"&gt;setCookie&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="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&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="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;browser&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="s2"&gt;setCookie can only be used in the browser, make sure to check for browser object first&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;currentDate&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;Date&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;farFutureDate&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;currentDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 100 years from now&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;farFutureDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUTCString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt; &lt;span class="o"&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;name&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;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; expires=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; path=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&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;At this point you might be wondering why do we need localstorage if we are going to use cookies. The answer is redundancy.&lt;/p&gt;

&lt;p&gt;And there you have it! This was how to persist data on clientside and avoid layout shifts in SSR.&lt;/p&gt;

&lt;h6&gt;If you have any comments, questions, or concerns, please keep them to yourself.&lt;/h6&gt;

&lt;h6&gt;All typos are on purpose.&lt;/h6&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kit.svelte.dev/docs/" rel="noopener noreferrer"&gt;SvelteKit Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chat.openai.com/" rel="noopener noreferrer"&gt;Unpaid Intern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRhzdY2LIXuzsiEvKVd-jxZYF17s4WwtvIIdA&amp;amp;usqp=CAU" rel="noopener noreferrer"&gt;Emotional Support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adelbeit.com/" rel="noopener noreferrer"&gt;Shameless Plug&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cover picture provided by &lt;a href="https://unsplash.com/photos/round-white-compass-iDzKdNI7Qgc?utm_content=creditShareLink&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>svelte</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>cookies</category>
    </item>
  </channel>
</rss>
