<?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: Vicente G. Reyes</title>
    <description>The latest articles on DEV Community by Vicente G. Reyes (@highcenburg).</description>
    <link>https://dev.to/highcenburg</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F126345%2F84bad9a2-d302-4943-8934-6c27a497daa1.png</url>
      <title>DEV Community: Vicente G. Reyes</title>
      <link>https://dev.to/highcenburg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/highcenburg"/>
    <language>en</language>
    <item>
      <title>Frontend and Backend of client #3 finally deployed to their respective cloud hosting providers! Now to test it..</title>
      <dc:creator>Vicente G. Reyes</dc:creator>
      <pubDate>Fri, 24 Apr 2026 10:05:58 +0000</pubDate>
      <link>https://dev.to/highcenburg/frontend-and-backend-of-client-3-finally-deployed-to-their-respective-cloud-hosting-providers-now-1jl9</link>
      <guid>https://dev.to/highcenburg/frontend-and-backend-of-client-3-finally-deployed-to-their-respective-cloud-hosting-providers-now-1jl9</guid>
      <description></description>
      <category>cloud</category>
      <category>showdev</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Hunting Disk Hogs on Ubuntu: A Shell Script for Finding the Largest Files</title>
      <dc:creator>Vicente G. Reyes</dc:creator>
      <pubDate>Tue, 21 Apr 2026 13:57:42 +0000</pubDate>
      <link>https://dev.to/highcenburg/hunting-disk-hogs-on-ubuntu-a-shell-script-for-finding-the-largest-files-52gk</link>
      <guid>https://dev.to/highcenburg/hunting-disk-hogs-on-ubuntu-a-shell-script-for-finding-the-largest-files-52gk</guid>
      <description>&lt;h2&gt;
  
  
  Why this script exists
&lt;/h2&gt;

&lt;p&gt;If you've ever watched your free disk space quietly shrink over a few weeks of active development, you know the feeling: yesterday you had plenty of headroom, today your IDE is yelling about low disk space, and you have no idea what ate the difference. Active Node.js and Python projects are especially good at this — &lt;code&gt;node_modules&lt;/code&gt;, build caches, &lt;code&gt;.next&lt;/code&gt; directories, virtual environments, and compiled artifacts accumulate silently with every install and every build.&lt;/p&gt;

&lt;p&gt;This article walks through a bash script, &lt;code&gt;find_largest_files.sh&lt;/code&gt;, that scans a directory tree and writes the largest files to a timestamped text report. It's designed to be a first diagnostic tool when you're trying to answer the question &lt;em&gt;"where did all my disk space go?"&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The script at a glance
&lt;/h2&gt;

&lt;p&gt;The full script is in &lt;code&gt;find_largest_files.sh&lt;/code&gt;. Here's what it does, step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Takes three optional arguments: search directory, number of results, and output filename.&lt;/li&gt;
&lt;li&gt;Validates that the search directory exists and that the count is a positive integer.&lt;/li&gt;
&lt;li&gt;Writes a header to the output file with timestamp, host, user, and scan parameters.&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;find&lt;/code&gt; to list every regular file under the target directory, along with its size in bytes.&lt;/li&gt;
&lt;li&gt;Sorts the list numerically by size (largest first), takes the top N, and converts byte counts into human-readable units (K/M/G/T).&lt;/li&gt;
&lt;li&gt;Appends the formatted list to the report and prints it to your terminal.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key design decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Using &lt;code&gt;find -printf&lt;/code&gt; instead of &lt;code&gt;ls&lt;/code&gt; or &lt;code&gt;du&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SEARCH_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\t%p\n'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;find -printf&lt;/code&gt; outputs size (&lt;code&gt;%s&lt;/code&gt;) in bytes and the full path (&lt;code&gt;%p&lt;/code&gt;), tab-separated. This matters for three reasons: byte-level precision means sorting stays accurate; tab separation survives filenames with spaces; and restricting to &lt;code&gt;-type f&lt;/code&gt; means we report on actual files, not directory aggregates the way &lt;code&gt;du&lt;/code&gt; would.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pruning pseudo-filesystems
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; /proc &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; /sys &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; /dev &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; /run &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; /snap &lt;span class="se"&gt;\)&lt;/span&gt; &lt;span class="nt"&gt;-prune&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/proc&lt;/code&gt;, &lt;code&gt;/sys&lt;/code&gt;, &lt;code&gt;/dev&lt;/code&gt;, and &lt;code&gt;/run&lt;/code&gt; are kernel-provided virtual filesystems. They contain "files" whose reported sizes are often meaningless (a &lt;code&gt;/proc/kcore&lt;/code&gt; can appear to be 128 TB). &lt;code&gt;/snap&lt;/code&gt; is pruned because snap mount points produce duplicate entries. Skipping all five keeps the report focused on real files on your actual disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Silencing permission errors
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a full &lt;code&gt;/&lt;/code&gt; scan, &lt;code&gt;find&lt;/code&gt; will hit directories your user can't read and print &lt;code&gt;Permission denied&lt;/code&gt; for every one of them — noise that can bury the real output. Redirecting stderr to &lt;code&gt;/dev/null&lt;/code&gt; cleans that up. Run the script with &lt;code&gt;sudo&lt;/code&gt; if you want complete coverage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Human-readable sizes in &lt;code&gt;awk&lt;/code&gt;, not &lt;code&gt;find&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We sort the raw byte counts first, &lt;em&gt;then&lt;/em&gt; format them in &lt;code&gt;awk&lt;/code&gt;. If we formatted early (e.g. via &lt;code&gt;find ... | sort -h&lt;/code&gt;), we'd either give up precision or depend on &lt;code&gt;sort -h&lt;/code&gt; parsing variants. Keeping bytes for sorting and converting after is simpler and portable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it against your scenario
&lt;/h2&gt;

&lt;p&gt;You mentioned roughly 30 GB of disk disappeared recently while you've been building &lt;code&gt;mortgage_system&lt;/code&gt; and &lt;code&gt;mortgage_frontend&lt;/code&gt; with Claude. Those kinds of projects are classic sources of silent disk bloat. Here's how I'd approach the investigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Get the big picture
&lt;/h3&gt;

&lt;p&gt;Start at root to confirm whether the missing space is actually inside your project folders, or somewhere else entirely (logs, Docker, snap revisions, trash, etc.).&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; ./find_largest_files.sh / 50 full_scan.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you the 50 biggest files system-wide. If most of them are under &lt;code&gt;/home/you/mortgage_system/...&lt;/code&gt; or &lt;code&gt;/home/you/mortgage_frontend/...&lt;/code&gt;, your instinct was right. If the top entries are elsewhere — &lt;code&gt;/var/lib/docker&lt;/code&gt;, &lt;code&gt;/var/log&lt;/code&gt;, &lt;code&gt;~/.cache&lt;/code&gt;, snap backups — the real culprit is somewhere you weren't looking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Focus on the suspects
&lt;/h3&gt;

&lt;p&gt;Once you've confirmed the project folders are the problem, narrow the scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./find_largest_files.sh ~/mortgage_system 30 mortgage_system_report.txt
./find_largest_files.sh ~/mortgage_frontend 30 mortgage_frontend_report.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Check directory-level size too
&lt;/h3&gt;

&lt;p&gt;Individual largest files tell one story; directory totals tell another. A million tiny files in &lt;code&gt;node_modules&lt;/code&gt; won't show up in a largest-files report, but they'll still eat gigabytes. Pair the script with &lt;code&gt;du&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nt"&gt;--max-depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 ~/mortgage_system | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rh&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nt"&gt;--max-depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 ~/mortgage_frontend | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rh&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common culprits in active dev folders
&lt;/h2&gt;

&lt;p&gt;Based on the stack you're likely using, here are the usual suspects, roughly in order of how often they're the answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;node_modules/&lt;/code&gt;&lt;/strong&gt; — routinely 500 MB to 2 GB &lt;em&gt;per project&lt;/em&gt;. Two full Next.js/React projects with heavy dependency trees can easily account for 3–5 GB each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.next/&lt;/code&gt; or &lt;code&gt;dist/&lt;/code&gt; or &lt;code&gt;build/&lt;/code&gt;&lt;/strong&gt; — production builds and incremental build caches. Next.js's &lt;code&gt;.next/cache&lt;/code&gt; in particular can grow to several GB over weeks of &lt;code&gt;npm run dev&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.git/&lt;/code&gt;&lt;/strong&gt; — if you've committed large binaries or have a long history, &lt;code&gt;.git/objects&lt;/code&gt; can be surprisingly fat. &lt;code&gt;git gc --aggressive&lt;/code&gt; helps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python &lt;code&gt;__pycache__/&lt;/code&gt; and &lt;code&gt;.venv/&lt;/code&gt;&lt;/strong&gt; — virtual environments with ML/data dependencies (torch, tensorflow, pandas) are often 3–8 GB each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker layers&lt;/strong&gt; — &lt;code&gt;/var/lib/docker&lt;/code&gt; is the single most common "where did my disk go?" answer on dev machines. &lt;code&gt;docker system df&lt;/code&gt; shows the breakdown; &lt;code&gt;docker system prune -a&lt;/code&gt; reclaims it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log files&lt;/strong&gt; — &lt;code&gt;/var/log/journal/&lt;/code&gt;, application logs, and PM2 logs can grow indefinitely if no rotation is configured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snap revisions&lt;/strong&gt; — Ubuntu keeps old snap versions by default. &lt;code&gt;sudo snap set system refresh.retain=2&lt;/code&gt; caps retention at two revisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trash&lt;/strong&gt; — &lt;code&gt;~/.local/share/Trash/&lt;/code&gt; is easy to forget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser caches&lt;/strong&gt; — &lt;code&gt;~/.cache/google-chrome&lt;/code&gt;, &lt;code&gt;~/.cache/mozilla&lt;/code&gt;, and similar can hit several GB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Node projects specifically, a quick sanity check:&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;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; ~/mortgage_system/node_modules ~/mortgage_frontend/node_modules 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If each is over a gigabyte, that's your ~30 GB budget explained between two projects, their build caches, and a &lt;code&gt;.git&lt;/code&gt; folder or two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful companion commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Overall disk usage at a glance&lt;/span&gt;
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;

&lt;span class="c"&gt;# Top-level directories sorted by size (run from /)&lt;/span&gt;
&lt;span class="nb"&gt;sudo du&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nt"&gt;--max-depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 / 2&amp;gt;/dev/null | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rh&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;span class="c"&gt;# What's eating your home directory&lt;/span&gt;
&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nt"&gt;--max-depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 ~ | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rh&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;span class="c"&gt;# Docker-specific&lt;/span&gt;
docker system &lt;span class="nb"&gt;df
&lt;/span&gt;docker system prune &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--volumes&lt;/span&gt;  &lt;span class="c"&gt;# aggressive, frees everything unused&lt;/span&gt;

&lt;span class="c"&gt;# Clean npm cache&lt;/span&gt;
npm cache clean &lt;span class="nt"&gt;--force&lt;/span&gt;

&lt;span class="c"&gt;# Clean pip cache&lt;/span&gt;
pip cache purge

&lt;span class="c"&gt;# Clear systemd journal older than 7 days&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;--vacuum-time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Going further: &lt;code&gt;ncdu&lt;/code&gt; for interactive exploration
&lt;/h2&gt;

&lt;p&gt;The script gives you a static report, which is great for archiving and diffing over time. For interactive drilling, install &lt;code&gt;ncdu&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ncdu
ncdu ~
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It gives you a terminal UI where you can navigate directories by size, delete things on the spot, and generally understand disk usage faster than any CLI combination. It's the tool I reach for once the script points me to the neighbourhood and I need to find the exact house.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up ongoing monitoring
&lt;/h2&gt;

&lt;p&gt;If you want to catch disk bloat as it happens instead of after the fact, schedule the script via cron and diff the reports:&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;# Edit your crontab&lt;/span&gt;
crontab &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;# Add: run every Sunday at 2 AM, save to a reports directory&lt;/span&gt;
0 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; 0 /home/you/find_largest_files.sh / 50 /home/you/disk_reports/scan_&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +&lt;span class="se"&gt;\%&lt;/span&gt;Y&lt;span class="se"&gt;\%&lt;/span&gt;m&lt;span class="se"&gt;\%&lt;/span&gt;d&lt;span class="si"&gt;)&lt;/span&gt;.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A week later, &lt;code&gt;diff&lt;/code&gt; two reports to see what grew.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.gnu.org/software/findutils/manual/html_mono/find.html" rel="noopener noreferrer"&gt;GNU findutils manual — find&lt;/a&gt; — authoritative reference for &lt;code&gt;find&lt;/code&gt;, including the full &lt;code&gt;-printf&lt;/code&gt; format spec.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man7.org/linux/man-pages/man1/du.1.html" rel="noopener noreferrer"&gt;Linux &lt;code&gt;du&lt;/code&gt; man page&lt;/a&gt; — directory-aggregate sizing, the natural complement to this script.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man7.org/linux/man-pages/man1/df.1.html" rel="noopener noreferrer"&gt;Linux &lt;code&gt;df&lt;/code&gt; man page&lt;/a&gt; — filesystem-level free space.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.yorhel.nl/ncdu" rel="noopener noreferrer"&gt;ncdu home page&lt;/a&gt; — interactive disk usage analyzer.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.docker.com/engine/manage-resources/pruning/" rel="noopener noreferrer"&gt;Docker: prune unused objects&lt;/a&gt; — official guide to reclaiming Docker space.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.npmjs.com/cli/v10/commands/npm-cache" rel="noopener noreferrer"&gt;npm cache documentation&lt;/a&gt; — how npm's cache works and how to clean it.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://askubuntu.com/questions/7738/how-to-find-the-largest-ten-files-on-the-hard-drive" rel="noopener noreferrer"&gt;Ask Ubuntu: Why is my disk full?&lt;/a&gt; — community thread with many alternative one-liners.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://wiki.archlinux.org/title/Disk_usage_analyzer" rel="noopener noreferrer"&gt;Arch Wiki: Disk usage analyzers&lt;/a&gt; — concise overview of CLI and GUI tools across the Linux ecosystem.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;If the script points at &lt;code&gt;node_modules&lt;/code&gt; and &lt;code&gt;.next&lt;/code&gt; inside your two mortgage projects, that's consistent with the 30 GB of lost disk — two mature JS/TS codebases with their build artifacts can account for exactly that range. The usual remediation is &lt;code&gt;rm -rf node_modules .next&lt;/code&gt; in each project, followed by a fresh &lt;code&gt;npm install&lt;/code&gt; only on the one you're actively working on. If instead the biggest files live under &lt;code&gt;/var/lib/docker&lt;/code&gt; or &lt;code&gt;/var/log&lt;/code&gt;, the fix is completely different, which is exactly why running a scan first beats guessing.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>ubuntu</category>
      <category>bash</category>
      <category>devops</category>
    </item>
    <item>
      <title>Renaming 1000+ Pages Worth of "Levels" Without Losing My Mind</title>
      <dc:creator>Vicente G. Reyes</dc:creator>
      <pubDate>Tue, 21 Apr 2026 05:02:35 +0000</pubDate>
      <link>https://dev.to/highcenburg/renaming-1000-pages-worth-of-levels-without-losing-my-mind-1cnk</link>
      <guid>https://dev.to/highcenburg/renaming-1000-pages-worth-of-levels-without-losing-my-mind-1cnk</guid>
      <description>&lt;p&gt;A message from a colleague dropped into my inbox one morning:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hi Ice, now that we pushed the changes sa site, we need to scour the site for mentions of Level 1, Level 2, Level 1/2 and change them accordingly to Level 1 → Intro to Neurofascial Training, Level 1/2 → Intermediate Instability Training, Level 2 → Advanced Neurofascial Training&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On the surface, this looks like a simple find-and-replace job. Read the message, open the WordPress admin, use the search feature, fix each page. Done before lunch.&lt;/p&gt;

&lt;p&gt;Then I actually opened the site and pulled the sitemap. &lt;strong&gt;1,074 URLs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That changes the math pretty quickly. Even if only a fraction of those pages mention "Level" anywhere, manually clicking through every post and page, ctrl-F-ing for three different strings, and then figuring out which instance needs which replacement — that's a full day of soul-crushing work at minimum, and a very real chance of missing something.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mapping
&lt;/h2&gt;

&lt;p&gt;Before anything else, I wrote the rename table down somewhere I couldn't lose it, because a misread mapping here would mean renaming correct text into incorrect text, which is strictly worse than leaving it alone:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Old text&lt;/th&gt;
&lt;th&gt;New text&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Level 1&lt;/td&gt;
&lt;td&gt;Intro to Neurofascial Training&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Level 1/2&lt;/td&gt;
&lt;td&gt;Intermediate Instability Training&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Level 2&lt;/td&gt;
&lt;td&gt;Advanced Neurofascial Training&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important ordering detail: "Level 1/2" has to be matched &lt;em&gt;before&lt;/em&gt; "Level 1", or a naive find-and-replace would turn "Level 1/2" into "Intro to Neurofascial Training/2". Easy to miss, painful to undo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use WordPress search-replace plugins?
&lt;/h2&gt;

&lt;p&gt;Plugins like Better Search Replace do exist, and in a simpler world I'd use one. But they operate on the database directly, which means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One wrong checkbox and you nuke every "Level 1" across post content, post meta, widget text, theme options, and anywhere else the phrase appears — including places it &lt;em&gt;shouldn't&lt;/em&gt; be replaced, like analytics or audit logs.&lt;/li&gt;
&lt;li&gt;There's no preview of &lt;em&gt;where&lt;/em&gt; each match lives. You're trusting the plugin's count and hoping nothing weird is hiding in a shortcode.&lt;/li&gt;
&lt;li&gt;The three strings overlap, so the order of operations matters and plugins don't always let you sequence replacements atomically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What I actually wanted was a map — a list of every occurrence with enough surrounding context to judge whether it's safe to change, plus a direct link to the WordPress editor for that specific page. The replacement itself I'd still do by hand, but informed by real data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;I wrote a Python script that walks the site's sitemap, visits each URL, and searches the rendered HTML for the three patterns using a single regex with word boundaries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;LEVEL_PATTERN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bLevel\s*1\s*/\s*2\b|\bLevel\s*1\b|\bLevel\s*2\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&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;Word boundaries matter here. Without &lt;code&gt;\b&lt;/code&gt;, a page mentioning "Level 10" or "Level 12-week program" would get flagged as a false "Level 1" match. And the alternation order mirrors the mapping problem above — "Level 1/2" is tried first so it doesn't get shadowed by the simpler "Level 1" pattern.&lt;/p&gt;

&lt;p&gt;For each match, the script captures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The page URL and &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The WordPress post ID, extracted from the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; class (WP adds &lt;code&gt;page-id-123&lt;/code&gt; or &lt;code&gt;postid-123&lt;/code&gt; automatically)&lt;/li&gt;
&lt;li&gt;A direct admin edit link built from that ID: &lt;code&gt;/wp-admin/post.php?post=123&amp;amp;action=edit&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;About 80 characters of context on either side of the match&lt;/li&gt;
&lt;li&gt;The HTML tag wrapping the match (&lt;code&gt;h2&lt;/code&gt;, &lt;code&gt;li&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt;, etc.) to help me find it inside the block editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of it dumped to a CSV. Rows sorted, duplicates within a page collapsed, system paths like &lt;code&gt;/wp-admin&lt;/code&gt; and &lt;code&gt;/wp-json&lt;/code&gt; filtered out so the scanner doesn't waste time on pages that aren't user content.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the CSV actually gives me
&lt;/h2&gt;

&lt;p&gt;Instead of 1,074 pages to click through blindly, I now have a focused list of every page that mentions "Level" in any form, with a one-click link to the editor and enough context to know which replacement to apply. The context column is the real magic — I can see a snippet like &lt;code&gt;…our flagship Level 1 class is held every Tuesday…&lt;/code&gt; and immediately know it's the class name that needs to become "Intro to Neurofascial Training", not some incidental use of the word "level".&lt;/p&gt;

&lt;p&gt;For the rare edge case — a page that mentions "Level 1" in a way that &lt;em&gt;shouldn't&lt;/em&gt; be renamed, like historical text or a testimonial quoting someone — I can spot it in the context column and skip that row. That judgment call is exactly the part you don't want a blind database replacement making on your behalf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The temptation with a task like this is to just start clicking. It feels productive. But on a site of any real size, a half hour spent writing a scanner pays for itself almost immediately — not just in saved time, but in the confidence that you actually caught everything and didn't quietly corrupt adjacent content along the way.&lt;/p&gt;

&lt;p&gt;Sometimes the most valuable thing automation gives you isn't speed. It's the audit trail.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;New XML Sitemaps Functionality in WordPress 5.5 — Make WordPress Core: &lt;a href="https://make.wordpress.org/core/2020/07/22/new-xml-sitemaps-functionality-in-wordpress-5-5/" rel="noopener noreferrer"&gt;https://make.wordpress.org/core/2020/07/22/new-xml-sitemaps-functionality-in-wordpress-5-5/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Sitemaps XML Protocol — sitemaps.org: &lt;a href="https://www.sitemaps.org/protocol.html" rel="noopener noreferrer"&gt;https://www.sitemaps.org/protocol.html&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;&lt;code&gt;body_class()&lt;/code&gt; Function Reference — WordPress Developer Resources: &lt;a href="https://developer.wordpress.org/reference/functions/body_class/" rel="noopener noreferrer"&gt;https://developer.wordpress.org/reference/functions/body_class/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;&lt;code&gt;get_body_class()&lt;/code&gt; Function Reference — WordPress Developer Resources: &lt;a href="https://developer.wordpress.org/reference/functions/get_body_class/" rel="noopener noreferrer"&gt;https://developer.wordpress.org/reference/functions/get_body_class/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;&lt;code&gt;re&lt;/code&gt; — Regular expression operations — Python 3 docs: &lt;a href="https://docs.python.org/3/library/re.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/re.html&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Better Search Replace — WordPress Plugin Directory: &lt;a href="https://wordpress.org/plugins/better-search-replace/" rel="noopener noreferrer"&gt;https://wordpress.org/plugins/better-search-replace/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Requests: HTTP for Humans — &lt;a href="https://requests.readthedocs.io/" rel="noopener noreferrer"&gt;https://requests.readthedocs.io/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Beautiful Soup Documentation — &lt;a href="https://www.crummy.com/software/BeautifulSoup/bs4/doc/" rel="noopener noreferrer"&gt;https://www.crummy.com/software/BeautifulSoup/bs4/doc/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>wordpress</category>
      <category>softwaredevelopment</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to Manually Backup WordPress Sites via SSH</title>
      <dc:creator>Vicente G. Reyes</dc:creator>
      <pubDate>Fri, 17 Apr 2026 06:14:35 +0000</pubDate>
      <link>https://dev.to/highcenburg/how-to-manually-backup-wordpress-sites-via-ssh-22o4</link>
      <guid>https://dev.to/highcenburg/how-to-manually-backup-wordpress-sites-via-ssh-22o4</guid>
      <description>&lt;p&gt;Backing up your WordPress site is one of the most important maintenance tasks you can do as a site owner. While plugins like UpdraftPlus or Jetpack make this easy, knowing how to do it &lt;strong&gt;manually via SSH&lt;/strong&gt; gives you full control — no third-party dependencies, no bloat, just a clean archive you own.&lt;/p&gt;

&lt;p&gt;This guide walks you through creating a full file backup of your WordPress site directly from the server using the command line.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installing the Required Tools
&lt;/h2&gt;

&lt;p&gt;Before connecting to your server, make sure the necessary tools are installed on your &lt;strong&gt;local machine&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  macOS
&lt;/h3&gt;

&lt;p&gt;macOS comes with &lt;code&gt;ssh&lt;/code&gt; and &lt;code&gt;scp&lt;/code&gt; pre-installed. No action needed — just open Terminal and you're ready to go.&lt;/p&gt;

&lt;h3&gt;
  
  
  Linux (Ubuntu/Debian)
&lt;/h3&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;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;openssh-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Windows
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Option A — Windows Subsystem for Linux (WSL)&lt;/strong&gt; &lt;em&gt;(recommended)&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--install&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once WSL is set up, &lt;code&gt;ssh&lt;/code&gt; and &lt;code&gt;scp&lt;/code&gt; are available inside the Linux shell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B — OpenSSH via PowerShell&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Add-WindowsCapability&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Online&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;OpenSSH.Client~~~~0.0.1.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing, &lt;code&gt;ssh&lt;/code&gt; and &lt;code&gt;scp&lt;/code&gt; will be available directly in PowerShell or Command Prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option C — GUI alternative&lt;/strong&gt;: Install &lt;a href="https://winscp.net/" rel="noopener noreferrer"&gt;WinSCP&lt;/a&gt; for a drag-and-drop interface to transfer files instead of using &lt;code&gt;scp&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  On the Server
&lt;/h3&gt;

&lt;p&gt;Your server should already have &lt;code&gt;tar&lt;/code&gt; and &lt;code&gt;mysqldump&lt;/code&gt; available. If for any reason they are missing, install them with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# tar&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install tar&lt;/span&gt;          &lt;span class="c"&gt;# Debian/Ubuntu&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;yum &lt;span class="nb"&gt;install tar&lt;/span&gt;          &lt;span class="c"&gt;# CentOS/RHEL&lt;/span&gt;

&lt;span class="c"&gt;# mysqldump (part of the MySQL client)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;mysql-client          &lt;span class="c"&gt;# Debian/Ubuntu&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;yum &lt;span class="nb"&gt;install &lt;/span&gt;mysql                 &lt;span class="c"&gt;# CentOS/RHEL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before you begin, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH access to your server&lt;/li&gt;
&lt;li&gt;The server's IP address&lt;/li&gt;
&lt;li&gt;Your SSH credentials (username and password, or an SSH key)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scp&lt;/code&gt; or an SFTP client installed on your local machine&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: SSH Into Your Server
&lt;/h2&gt;

&lt;p&gt;Open your terminal and connect to your server using SSH. Replace &lt;code&gt;ip_address&lt;/code&gt; with your actual server IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@ip_address
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll be prompted for your password (or authenticated via SSH key). Once connected, you'll be inside your server's shell.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; If you're using a non-root user, replace &lt;code&gt;root&lt;/code&gt; with your username (e.g., &lt;code&gt;ssh deploy@192.168.1.100&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 2: Create a Compressed Archive of Your Site
&lt;/h2&gt;

&lt;p&gt;Your WordPress files typically live inside the &lt;code&gt;public_html&lt;/code&gt; directory. The following command creates a compressed &lt;code&gt;.tar.gz&lt;/code&gt; archive of the entire folder:&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;tar&lt;/span&gt; &lt;span class="nt"&gt;-czvf&lt;/span&gt; ~/public_html/backup-site.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; ~/ public_html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What each flag does:
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a new archive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compress with gzip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verbose output (shows files being archived)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Specifies the output filename&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-C ~/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changes to the home directory before archiving&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The backup file &lt;code&gt;backup-site.tar.gz&lt;/code&gt; will be saved inside &lt;code&gt;~/public_html/&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Depending on your site size, this may take a few minutes. Large media libraries will increase the archive size significantly.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 3: Download the Backup to Your Local Machine
&lt;/h2&gt;

&lt;p&gt;Once the archive is created, exit the SSH session and run the following &lt;code&gt;scp&lt;/code&gt; command on your &lt;strong&gt;local machine&lt;/strong&gt; to download the backup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scp root@ip_address:~/public_html/backup-site.tar.gz ~/Downloads/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This securely copies the file from your server to your local &lt;code&gt;~/Downloads/&lt;/code&gt; folder.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; If you're on Windows, you can use &lt;a href="https://winscp.net/" rel="noopener noreferrer"&gt;WinSCP&lt;/a&gt; or the built-in &lt;code&gt;scp&lt;/code&gt; command in PowerShell/WSL as an alternative.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 4: Don't Forget the Database
&lt;/h2&gt;

&lt;p&gt;A full WordPress backup requires &lt;strong&gt;both the files and the database&lt;/strong&gt;. Your files backup covers themes, plugins, and uploads — but your posts, pages, and settings live in MySQL.&lt;/p&gt;

&lt;p&gt;To export your database, run this on the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysqldump &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; your_database_name &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/public_html/backup-db.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then download it the same 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 root@ip_address:~/public_html/backup-db.sql ~/Downloads/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;your_database_name&lt;/code&gt; with the database name found in your &lt;code&gt;wp-config.php&lt;/code&gt; file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Clean Up the Server
&lt;/h2&gt;

&lt;p&gt;To avoid using up disk space on your server, delete the backup files after downloading them:&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;rm&lt;/span&gt; ~/public_html/backup-site.tar.gz
&lt;span class="nb"&gt;rm&lt;/span&gt; ~/public_html/backup-db.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Restoring from a Backup
&lt;/h2&gt;

&lt;p&gt;To restore, simply reverse the process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload the &lt;code&gt;.tar.gz&lt;/code&gt; file back to the server using &lt;code&gt;scp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Extract it with:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzvf&lt;/span&gt; backup-site.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; ~/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Import the database with:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; your_database_name &amp;lt; backup-db.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Manual backups via SSH are reliable, fast, and give you a portable snapshot of your entire site. For production sites, consider automating this process with a cron job or combining it with offsite storage like S3 or Google Drive.&lt;/p&gt;

&lt;p&gt;A backup you never tested is a backup you can't trust — make sure to do a test restore at least once.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>linux</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>7/9 implementations done quick!</title>
      <dc:creator>Vicente G. Reyes</dc:creator>
      <pubDate>Mon, 13 Apr 2026 18:36:37 +0000</pubDate>
      <link>https://dev.to/highcenburg/79-implementations-done-quick-39jh</link>
      <guid>https://dev.to/highcenburg/79-implementations-done-quick-39jh</guid>
      <description></description>
      <category>coding</category>
      <category>devchallenge</category>
      <category>devjournal</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built an Enterprise Coffee Dashboard (That Refuses to Brew Coffee)</title>
      <dc:creator>Vicente G. Reyes</dc:creator>
      <pubDate>Sat, 04 Apr 2026 08:08:30 +0000</pubDate>
      <link>https://dev.to/highcenburg/i-built-an-enterprise-coffee-dashboard-that-refuses-to-brew-coffee-2gik</link>
      <guid>https://dev.to/highcenburg/i-built-an-enterprise-coffee-dashboard-that-refuses-to-brew-coffee-2gik</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/aprilfools-2026"&gt;DEV April Fools Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built the &lt;strong&gt;Enterprise HTCPCP Interface (HTCPCP_CTRL_NODE_v1.0)&lt;/strong&gt;. It is a highly over-engineered, unnecessarily complex, and deeply frustrating dashboard for brewing coffee using the Hyper Text Coffee Pot Control Protocol (RFC 2324).&lt;/p&gt;

&lt;p&gt;It solves absolutely zero real-world problems and introduces several new ones:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cross-Contaminating Sliders:&lt;/strong&gt; Trying to set the "Thermal Kinetic Energy" (temperature)? Oops, that just randomly altered your "Extraction Pressure". Adjusting the "Particulate Granularity"? Say goodbye to your "Lactose Aeration Quotient". Getting the perfect brew settings is a Sisyphean task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual Hand-Crank Power Generator:&lt;/strong&gt; You can't just click "Brew". You have to rapidly mash a button to charge the system's power capacitor to 100%. If you stop clicking, the power drains back to zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inevitable Failure:&lt;/strong&gt; After all that hard work, the system connects to a sentient AI teapot that &lt;em&gt;always&lt;/em&gt; rejects your request with a dramatic, dynamically generated HTTP 418 error.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;You can try to brew some coffee (and inevitably fail) here:&lt;br&gt;
&lt;strong&gt;&lt;a&gt;Live Demo: Enterprise HTCPCP Interface&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Warning: May cause mild frustration and a sudden craving for tea).&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The project was built entirely within Google AI Studio. Here is a snippet of the delightfully terrible UX logic where the sliders sabotage each other, and the Gemini AI prompt that generates the 418 excuse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The delightfully terrible cross-contaminating sliders&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleTempChange&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ChangeEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setTemp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&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="nx"&gt;target&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setPressure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))));&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleGrindChange&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ChangeEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setGrind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&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="nx"&gt;target&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setAeration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))));&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// ... (other sliders do the same)&lt;/span&gt;

  &lt;span class="c1"&gt;// The AI Sentient Teapot Prompt&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`You are a sentient teapot that has just received a highly complex, over-engineered request to brew coffee via the HTCPCP (Hyper Text Coffee Pot Control Protocol). 
  You must reject this request with a 418 I'm a teapot error. 
  Generate a highly dramatic, passive-aggressive, and overly technical excuse explaining why you cannot brew coffee because you are, in fact, a teapot. 
  Mention Larry Masinter (the creator of the 418 status code) in a reverent or funny way.
  Keep it under 3 sentences.`&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gemini-3-flash-preview&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&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;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;I built this using &lt;strong&gt;Google AI Studio's&lt;/strong&gt; agentic build environment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js App Router, React, and Tailwind CSS for that overly serious, dark-mode "enterprise hacker" aesthetic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animations:&lt;/strong&gt; motion (Framer Motion) for the dramatic terminal scanlines, the flashing 418 error screen, and the floating tea emoji.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Integration:&lt;/strong&gt; The @google/genai SDK. I used the gemini-3-flash-preview model to act as the backend "appliance". Instead of actually brewing coffee, it generates a unique, passive-aggressive excuse every single time you hit the brew button.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Category
&lt;/h2&gt;

&lt;p&gt;I am submitting this for &lt;strong&gt;two&lt;/strong&gt; prize categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Best Google AI Usage:&lt;/strong&gt; I used Google AI Studio to build the app, and I embedded the Gemini API (gemini-3-flash-preview) directly into the application logic. Instead of using AI to solve a complex problem, I used cutting-edge generative AI to roleplay as a stubborn, sentient teapot that refuses to do its job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Ode to Larry Masinter:&lt;/strong&gt; The entire application is a massive, over-engineered tribute to HTCPCP and the 418 status code. Furthermore, the Gemini system prompt explicitly instructs the AI to mention Larry Masinter in a reverent or funny way in every single error message it generates!&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>devchallenge</category>
      <category>418challenge</category>
      <category>showdev</category>
    </item>
    <item>
      <title>The fine.dev site is showing Deployment Paused</title>
      <dc:creator>Vicente G. Reyes</dc:creator>
      <pubDate>Fri, 03 Apr 2026 13:26:52 +0000</pubDate>
      <link>https://dev.to/highcenburg/the-finedev-site-is-showing-deployment-paused-2k60</link>
      <guid>https://dev.to/highcenburg/the-finedev-site-is-showing-deployment-paused-2k60</guid>
      <description></description>
      <category>cicd</category>
      <category>devops</category>
      <category>news</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Should I stress over a staging environment?</title>
      <dc:creator>Vicente G. Reyes</dc:creator>
      <pubDate>Fri, 03 Apr 2026 13:11:53 +0000</pubDate>
      <link>https://dev.to/highcenburg/should-i-stress-over-a-staging-environment-2chp</link>
      <guid>https://dev.to/highcenburg/should-i-stress-over-a-staging-environment-2chp</guid>
      <description></description>
      <category>cicd</category>
      <category>devops</category>
      <category>discuss</category>
      <category>testing</category>
    </item>
    <item>
      <title>Post-Mortem: The March 2026 Axios Supply Chain Attack</title>
      <dc:creator>Vicente G. Reyes</dc:creator>
      <pubDate>Tue, 31 Mar 2026 11:38:35 +0000</pubDate>
      <link>https://dev.to/highcenburg/post-mortem-the-march-2026-axios-supply-chain-attack-352f</link>
      <guid>https://dev.to/highcenburg/post-mortem-the-march-2026-axios-supply-chain-attack-352f</guid>
      <description>&lt;h2&gt;
  
  
  The Incident
&lt;/h2&gt;

&lt;p&gt;On March 31, 2026, a high-profile supply chain attack targeted &lt;strong&gt;Axios&lt;/strong&gt;, a critical HTTP client for the JavaScript ecosystem. By hijacking a maintainer's NPM account, attackers injected a malicious dependency, &lt;code&gt;plain-crypto-js&lt;/code&gt;, which deployed a cross-platform Remote Access Trojan (RAT).&lt;/p&gt;




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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;th&gt;Information&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;axios@1.14.1&lt;/code&gt;, &lt;code&gt;axios@0.30.4&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Malicious Dependency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;plain-crypto-js@4.2.1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Payload&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cross-platform RAT (Linux, macOS, Windows)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C2 Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sfrclak.com:8000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resolution Window&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Live for ~3 hours (00:21 – 03:29 UTC)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Technical Deep Dive
&lt;/h2&gt;

&lt;p&gt;The attack bypassed standard security audits by hiding the malicious logic within a sub-dependency. Once installed via a standard &lt;code&gt;npm install&lt;/code&gt;, the payload scanned the host machine for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Environment Variables:&lt;/strong&gt; &lt;code&gt;.env&lt;/code&gt; files and active shell exports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth Tokens:&lt;/strong&gt; &lt;code&gt;~/.npmrc&lt;/code&gt; and &lt;code&gt;~/.aws/credentials&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH Keys:&lt;/strong&gt; Unprotected private keys in &lt;code&gt;~/.ssh&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data was exfiltrated via &lt;strong&gt;POST requests&lt;/strong&gt; to the &lt;code&gt;sfrclak.com&lt;/code&gt; Command &amp;amp; Control (C2) server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Remediation &amp;amp; Verification
&lt;/h2&gt;

&lt;p&gt;To ensure a development environment is sanitized, the following protocol was executed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Network Sinkholing:&lt;/strong&gt; Manually mapping the C2 domain to &lt;code&gt;127.0.0.1&lt;/code&gt; in &lt;code&gt;/etc/hosts&lt;/code&gt; to prevent further exfiltration and "kill" the phone-home capability.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lockfile Audit:&lt;/strong&gt; Scanning all local projects for traces of the malicious package using a space-safe search:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"package-lock.json"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"yarn.lock"&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; &lt;span class="nt"&gt;-print0&lt;/span&gt; | xargs &lt;span class="nt"&gt;-0&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"plain-crypto-js"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Environment Sanitization:&lt;/strong&gt; Clearing the global NPM cache and updating tool managers (like &lt;code&gt;mise&lt;/code&gt;) to ensure only verified versions are used moving forward.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro-Tip:&lt;/strong&gt; Always use &lt;code&gt;npm audit&lt;/code&gt; or tools like &lt;strong&gt;Snyk&lt;/strong&gt; to monitor your dependency tree for "hidden" sub-dependencies that do not appear directly in your &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>node</category>
    </item>
    <item>
      <title>Claude Code vs Antigravity?</title>
      <dc:creator>Vicente G. Reyes</dc:creator>
      <pubDate>Thu, 26 Mar 2026 05:34:23 +0000</pubDate>
      <link>https://dev.to/highcenburg/claude-code-vs-antigravity-506e</link>
      <guid>https://dev.to/highcenburg/claude-code-vs-antigravity-506e</guid>
      <description>&lt;p&gt;Who you got? Claude or Antigravity? I read Claude is good for the frontend, Antigravity for the backend. Is that true? What are your thoughts?&lt;/p&gt;

</description>
      <category>devdiscuss</category>
      <category>ai</category>
    </item>
    <item>
      <title>Sorting Hashnode Series Posts: How to Display the Latest Post First</title>
      <dc:creator>Vicente G. Reyes</dc:creator>
      <pubDate>Wed, 25 Mar 2026 12:51:39 +0000</pubDate>
      <link>https://dev.to/highcenburg/sorting-hashnode-series-posts-how-to-display-the-latest-post-first-5ek3</link>
      <guid>https://dev.to/highcenburg/sorting-hashnode-series-posts-how-to-display-the-latest-post-first-5ek3</guid>
      <description>&lt;p&gt;When you publish a series of articles on your Hashnode blog and consume it via their GraphQL API for a custom portfolio or website, you quickly run into a common roadblock: &lt;strong&gt;Hashnode’s API natively returns series posts in chronological order (oldest first)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;While this makes sense for consecutive tutorials ("Part 1", "Part 2"), it’s significantly less ideal for ongoing series—like my "Learning how to code with AI" series—where readers typically want to see your newest breakthroughs or the latest problem you solved directly at the top of the feed.&lt;/p&gt;

&lt;p&gt;Unfortunately, looking at the Hashnode GraphQL Schema (&lt;code&gt;Series.posts&lt;/code&gt;), there isn't an out-of-the-box &lt;code&gt;sort: RECENT&lt;/code&gt; parameter available. Instead, it demands that you sequentially traverse the cursor paginations starting from the oldest post you've written in that series.&lt;/p&gt;

&lt;p&gt;Here's how I solved this issue on my React/Vite portfolio to seamlessly deliver a newest-first reading experience to my users.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Oldest First Pagination
&lt;/h2&gt;

&lt;p&gt;By default, the standard query to fetch series posts looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;publication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;publishedAt&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;pageInfo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;endCursor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hasNextPage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns standard edge/node responses, but starting with the very first post of the series. To get the newest post to display on page 1 of our UI, we need the &lt;em&gt;end&lt;/em&gt; of the dataset instead of the beginning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: A Client-Side Cache &amp;amp; Array Reversal
&lt;/h2&gt;

&lt;p&gt;Since Hashnode API's &lt;code&gt;first&lt;/code&gt; parameter maxes out at &lt;code&gt;20&lt;/code&gt; items per request, we can't simply request &lt;code&gt;first: 1000&lt;/code&gt; and reverse it in one go. We need to sequentially buffer the entire series.&lt;/p&gt;

&lt;p&gt;To keep it blazing fast for users querying consecutive pages, we shouldn't fetch the whole thing from scratch every time they click "Next". Instead, we can introduce a local caching mechanism in our code.&lt;/p&gt;

&lt;p&gt;We'll parse through all the posts via a &lt;code&gt;while&lt;/code&gt; loop on the initial page load, cache the fully assembled series, &lt;code&gt;.reverse()&lt;/code&gt; the data natively in JavaScript, and handle React's generic cursor logic over our local cache instead!&lt;/p&gt;

&lt;h3&gt;
  
  
  Re-writing &lt;code&gt;fetchSeries&lt;/code&gt; in TypeScript
&lt;/h3&gt;

&lt;p&gt;Here's my updated &lt;code&gt;lib/hashnode.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BlogPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BlogSeries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PageInfo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Let's declare local caches outside the function&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seriesMetaCache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Omit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BlogSeries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seriesPostsCache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BlogPost&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchSeries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BlogSeries&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BlogPost&lt;/span&gt; &lt;span class="p"&gt;}[];&lt;/span&gt; &lt;span class="nl"&gt;pageInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PageInfo&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="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;// 1. If no 'after' is provided, we must be loading the series for the first time.&lt;/span&gt;
  &lt;span class="c1"&gt;// We'll hit the Hashnode API repeatedly until we traverse all oldest-first pages.&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;after&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;seriesPostsCache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;slug&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="na"&gt;allPosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BlogPost&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hasNextPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;endCursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;seriesMeta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Omit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BlogSeries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasNextPage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;gqlFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SERIES_QUERY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PUBLICATION_HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;endCursor&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;publishedSeries&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;publication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;series&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;publishedSeries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Persist metadata on the first run&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;seriesMeta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;metaRest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;publishedSeries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;seriesMeta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;metaRest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;publishedSeries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;allPosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allPosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

      &lt;span class="nx"&gt;hasNextPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasNextPage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;endCursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endCursor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Here's the magic trick. Reverse the array so Newest is First.&lt;/span&gt;
    &lt;span class="nx"&gt;seriesPostsCache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allPosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reverse&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;seriesMeta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;seriesMetaCache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;seriesMeta&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="c1"&gt;// 3. Grab from cache&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allPosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;seriesPostsCache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;slug&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;seriesMeta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;seriesMetaCache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;slug&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;seriesMeta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Implement local slicing based on the provided cursors&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;allPosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Safety net in case of invalid cursor&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validStartIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;startIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;startIndex&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;allPosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allPosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validStartIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;validStartIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;first&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;nextHasNextPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;validStartIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;allPosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Set the newest 'endCursor' for the client to use on consecutive requests&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newEndCursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 5. Structure exactly how the calling React component expects&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;series&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;seriesMeta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
        &lt;span class="na"&gt;pageInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;endCursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newEndCursor&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;hasNextPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nextHasNextPage&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this approach is awesome:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero UI Adjustments&lt;/strong&gt;: The React component responsible for consuming the posts (&lt;code&gt;SeriesPage.tsx&lt;/code&gt;) doesn’t even know this under-the-hood wizardry is occurring. It passes &lt;code&gt;first&lt;/code&gt; and &lt;code&gt;after&lt;/code&gt; standardly and receives seamless newest-first data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant "Load More"&lt;/strong&gt;: Once the initial batch is traversed during &lt;code&gt;while(hasNextPage)&lt;/code&gt;, it acts exactly like an instant static array. Any subsequent "Load More" clicks don't hit the standard network stack; they instantly slice through &lt;code&gt;seriesPostsCache&lt;/code&gt; in micro-seconds!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overcoming GraphQL Limitations&lt;/strong&gt;: Sometimes headless CMS or GraphQL servers (like Hashnode's otherwise fantastic API) restrict complex filtering or sorting on nested nodes purely for their own database performance. When you abstract that layer on your backend caching structure, you win full control back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, whenever users visit the series pages in my portfolio, the most recently solved programming challenges are rightfully spotlighted at the very top. What's not to love about building digital experiences that slap?&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>graphql</category>
      <category>api</category>
    </item>
    <item>
      <title>Building a Seamless JWT Onboarding Flow with React Router v7 and Django</title>
      <dc:creator>Vicente G. Reyes</dc:creator>
      <pubDate>Tue, 17 Mar 2026 10:47:36 +0000</pubDate>
      <link>https://dev.to/highcenburg/building-a-seamless-jwt-onboarding-flow-with-react-router-v7-and-django-6bc</link>
      <guid>https://dev.to/highcenburg/building-a-seamless-jwt-onboarding-flow-with-react-router-v7-and-django-6bc</guid>
      <description>&lt;p&gt;Authentication and onboarding are often the highest-friction points in a new user's journey. If the process is clunky or requires too many redirects, users drop off. Recently, I set out to build a streamlined &lt;strong&gt;JWT authentication and onboarding flow&lt;/strong&gt; using a modern stack: &lt;strong&gt;React Router v7 (with Vite), TypeScript, and Django REST Framework (DRF)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk through the architecture, the backend implementation, and how to handle protected routes securely on the client side.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture Stack
&lt;/h2&gt;

&lt;p&gt;Here is what we are working with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Backend:&lt;/strong&gt; Django, Django REST Framework (DRF), and &lt;code&gt;djangorestframework-simplejwt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Frontend:&lt;/strong&gt; React (via Vite) and the newly released React Router v7.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Styling:&lt;/strong&gt; Tailwind CSS.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;State Management:&lt;/strong&gt; React Context API for lightweight auth state handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Goal
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; A user registers.&lt;/li&gt;
&lt;li&gt; The user logs in. They receive a short-lived access token and a refresh token.&lt;/li&gt;
&lt;li&gt; The backend knows if the user has completed their profile setup (&lt;code&gt;is_onboarded&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; If they haven't onboarded, the backend middleware blocks API requests, and the frontend automatically reroutes them to the &lt;code&gt;/onboarding&lt;/code&gt; page.&lt;/li&gt;
&lt;li&gt; Once onboarded, new tokens are issued with the updated status, and the user gains full access to the application &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. Setting up the Django Backend
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Custom User Model
&lt;/h3&gt;

&lt;p&gt;First, we need to track whether a user has finished setting up their profile. We extend Django's &lt;code&gt;AbstractUser&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# users/models.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.contrib.auth.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AbstractUser&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AbstractUser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;is_onboarded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BooleanField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Customizing the JWT Payload
&lt;/h3&gt;

&lt;p&gt;By default, SimpleJWT only includes the &lt;code&gt;user_id&lt;/code&gt; inside the token. We want the frontend to immediately know the user's name and onboarding status without making an extra API call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# users/serializers.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rest_framework_simplejwt.serializers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TokenObtainPairSerializer&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyTokenObtainPairSerializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TokenObtainPairSerializer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@classmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Inject custom claims into the JWT payload
&lt;/span&gt;        &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;is_onboarded&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_onboarded&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enforcing Onboarding with Middleware
&lt;/h3&gt;

&lt;p&gt;We want to strictly prevent un-onboarded users from accessing core application data. We do this via a custom Django middleware that intercepts requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# users/middleware.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.http&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JsonResponse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.urls&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OnboardingMiddleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_response&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Exempt authentication and onboarding endpoints
&lt;/span&gt;        &lt;span class="n"&gt;exempt_urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;token_obtain_pair&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/api/onboard/submit/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/api/register/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exempt_urls&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/admin/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;
        &lt;span class="c1"&gt;# Block access if authenticated but not onboarded
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_authenticated&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_onboarded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JsonResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ONBOARDING_REQUIRED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Please complete onboarding before accessing this resource.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, even if a user tries to bypass the frontend routing, the API will refuse to serve them data!&lt;/p&gt;

&lt;h3&gt;
  
  
  Refreshing Tokens upon Onboarding
&lt;/h3&gt;

&lt;p&gt;When the user submits the onboarding form, their status changes in the database. However, their physical JWT won't update automatically. We handle this by issuing completely new tokens in the onboarding response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# users/views.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rest_framework.views&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;APIView&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rest_framework.response&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rest_framework&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;permissions&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OnboardSubmitView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;APIView&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;permission_classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAuthenticated&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_onboarded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Already onboarded.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_onboarded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="n"&gt;user&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="c1"&gt;# Generate fresh tokens reflecting the new state
&lt;/span&gt;        &lt;span class="n"&gt;refresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyTokenObtainPairSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Onboarding complete.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_onboarded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refresh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_200_OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Setting up the React Frontend
&lt;/h2&gt;

&lt;p&gt;We spun up the frontend using React Router v7's built-in Vite template.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bypassing CORS locally
&lt;/h3&gt;

&lt;p&gt;To avoid CORS issues during development, we configured Vite to proxy &lt;code&gt;/api&lt;/code&gt; requests to our Django dev server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&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;vite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// plugins...&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;changeOrigin&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="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Global Authentication Context
&lt;/h3&gt;

&lt;p&gt;We use React Context to provide user data (&lt;code&gt;token&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt; payload, &lt;code&gt;login&lt;/code&gt;, &lt;code&gt;logout&lt;/code&gt;) globally. &lt;/p&gt;

&lt;p&gt;A critical detail: &lt;strong&gt;Server-Side Rendering (SSR)&lt;/strong&gt;. React Router v7 utilizes SSR by default when hydrating the app. If you try to read &lt;code&gt;localStorage.getItem('access_token')&lt;/code&gt; immediately on the server, Node.js will crash with a &lt;code&gt;ReferenceError: localStorage is not defined&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;We fix this by waiting for hydration on the client using a simple &lt;code&gt;useEffect&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/contexts/AuthContext.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ... interfaces and parseJwt helper ...&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;AuthProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setToken&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsClient&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Safe SSR hydration&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&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="nf"&gt;setIsClient&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="nf"&gt;setToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&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="s1"&gt;access_token&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="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="p"&gt;[]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parseJwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;UserPayload&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&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;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;access_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;else&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;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;access_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;token&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;login&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&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;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;refresh_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;access&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;isClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Prevent hydration mismatch&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AuthContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refreshAccessToken&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&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;/AuthContext.Provider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Route Protection and Redirection
&lt;/h3&gt;

&lt;p&gt;With our context in place, protecting routes and enforcing onboarding becomes incredibly simple. We intercept users in a &lt;code&gt;useEffect&lt;/code&gt; on our page components. &lt;/p&gt;

&lt;p&gt;Here is what our protected &lt;strong&gt;Home Dashboard&lt;/strong&gt; looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/routes/home.tsx&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;useEffect&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;react&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;useNavigate&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;react-router&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;useAuth&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;../contexts/AuthContext&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logout&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuth&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;navigate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNavigate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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="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;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;               &lt;span class="c1"&gt;// Kick out unauthenticated users&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;user&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;is_onboarded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/onboarding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// Kick out un-onboarded users&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;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="c1"&gt;// Don't render until verified to prevent brief flashes of content&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;user&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;is_onboarded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&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;Welcome&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;your&lt;/span&gt; &lt;span class="nx"&gt;portal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&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="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the user is pushed to &lt;code&gt;/onboarding&lt;/code&gt;, they hit our submit button. The API returns the &lt;em&gt;new&lt;/em&gt; tokens, we update the context, and React Router instantly pushes them back to &lt;code&gt;/&lt;/code&gt; seamlessly!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Inside Onboarding component submit handler&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleComplete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/onboard/submit/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;login&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;access&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;refresh&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Instantly updates global context&lt;/span&gt;
    &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&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="c1"&gt;// Redirect to dashboard&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;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By embedding the &lt;code&gt;is_onboarded&lt;/code&gt; claim directly inside the JWT, we eliminate the need for the frontend to constantly ping the database to check a user's status. Coupling this tightly with Django Middleware ensures backend security, while utilizing React Context provides snappy, seamless redirects on the frontend.&lt;/p&gt;

&lt;p&gt;Building auth doesn't have to be painful—with the right architecture, it can be safe, scalable, and a great experience for the end user.&lt;/p&gt;

</description>
      <category>python</category>
      <category>django</category>
      <category>react</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
