<?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: Aarush Karak</title>
    <description>The latest articles on DEV Community by Aarush Karak (@3ni8ma).</description>
    <link>https://dev.to/3ni8ma</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4076659%2F5780d02b-9a82-42fe-a961-a5c540bb29e5.png</url>
      <title>DEV Community: Aarush Karak</title>
      <link>https://dev.to/3ni8ma</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/3ni8ma"/>
    <language>en</language>
    <item>
      <title>A One-Command Pre-Flight Checklist for Developers: astro-tasks</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Sat, 19 Sep 2026 01:24:36 +0000</pubDate>
      <link>https://dev.to/3ni8ma/a-one-command-pre-flight-checklist-for-developers-astro-tasks-3cg6</link>
      <guid>https://dev.to/3ni8ma/a-one-command-pre-flight-checklist-for-developers-astro-tasks-3cg6</guid>
      <description>&lt;p&gt;Before starting work I used to open three tabs: GitHub notifications, the WakaTime dashboard, and a &lt;code&gt;git status&lt;/code&gt; sweep across local repos. I built &lt;a href="https://github.com/3ni8ma/astro-tasks" rel="noopener noreferrer"&gt;astro-tasks&lt;/a&gt; to put that in one terminal command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;astro-tasks
astro check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's GitHub notifications + open PRs (via the &lt;code&gt;gh&lt;/code&gt; CLI), coding stats for the last 7 days (total, daily average, top projects, languages via WakaTime), and a local repo health scan (branch, dirty state, unpushed commits).&lt;/p&gt;

&lt;p&gt;Subcommands cover the subsets: &lt;code&gt;astro scan&lt;/code&gt; for repos only, &lt;code&gt;astro log&lt;/code&gt; for coding stats, &lt;code&gt;astro config&lt;/code&gt; for configuration. &lt;code&gt;astro check --json&lt;/code&gt; emits the same data as machine-readable JSON, so you can pipe it into other tools.&lt;/p&gt;

&lt;p&gt;Caveats, stated honestly: it shells out to &lt;code&gt;gh&lt;/code&gt;, so you need &lt;code&gt;gh auth login&lt;/code&gt;, and coding stats need a WakaTime config at &lt;code&gt;~/.wakatime.cfg&lt;/code&gt;. Python 3.8+, MIT licensed.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/3ni8ma/astro-tasks" rel="noopener noreferrer"&gt;https://github.com/3ni8ma/astro-tasks&lt;/a&gt; — feedback welcome, especially on the JSON schema and multi-machine WakaTime setups.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>python</category>
      <category>cli</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Ship Mobile Apps for $0: Vercel + Render + Supabase Free Tier</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Sat, 12 Sep 2026 19:39:35 +0000</pubDate>
      <link>https://dev.to/3ni8ma/i-ship-mobile-apps-for-0-vercel-render-supabase-free-tier-27n5</link>
      <guid>https://dev.to/3ni8ma/i-ship-mobile-apps-for-0-vercel-render-supabase-free-tier-27n5</guid>
      <description>&lt;p&gt;TL;DR: My entire mobile stack costs $0 a month. Expo builds the app. Supabase holds data and auth. Render runs background jobs. Vercel hosts the landing page. Free tiers cover all of it. The table below shows every piece, its limit, and the catch. Steal this setup.&lt;/p&gt;

&lt;p&gt;My infrastructure bill is zero dollars. Not "free trial" zero. Not "first year" zero. Actual, ongoing, every-month zero. I ship a real mobile app with real users on free tiers. This post is the full receipt.&lt;/p&gt;

&lt;p&gt;People assume shipping an app costs money. Servers. Databases. Auth. Hosting. It sounds like hundreds a month. It is not. The free tiers in 2026 are absurdly generous. You can go from idea to production without touching a credit card. I know because I did.&lt;/p&gt;

&lt;p&gt;Here is the whole stack in one table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;th&gt;The catch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mobile app&lt;/td&gt;
&lt;td&gt;Expo + EAS&lt;/td&gt;
&lt;td&gt;Free builds, OTA updates&lt;/td&gt;
&lt;td&gt;Build queues get slow at peak hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database + Auth&lt;/td&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;td&gt;500MB Postgres, 50k monthly users&lt;/td&gt;
&lt;td&gt;Project pauses after 7 idle days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background jobs&lt;/td&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;750 free hours monthly&lt;/td&gt;
&lt;td&gt;Spins down after 15 min idle, slow cold start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Landing page&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;100GB bandwidth, auto SSL&lt;/td&gt;
&lt;td&gt;Build minutes capped monthly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code hosting + CI&lt;/td&gt;
&lt;td&gt;GitHub&lt;/td&gt;
&lt;td&gt;Unlimited public repos, 2000 CI minutes&lt;/td&gt;
&lt;td&gt;Private repo minutes burn faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publishing pipeline&lt;/td&gt;
&lt;td&gt;This repo&lt;/td&gt;
&lt;td&gt;$0 forever&lt;/td&gt;
&lt;td&gt;You write the posts yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total: $0. Catches: manageable. Let me walk through each one.&lt;/p&gt;

&lt;p&gt;Expo first. The app itself. Expo's free tier includes builds and over-the-air updates. OTA updates are the killer feature. I push JavaScript fixes and users get them instantly. No app store review. No waiting three days for Apple to approve a typo fix. Native changes still need a rebuild. JS changes just flow.&lt;/p&gt;

&lt;p&gt;The catch: EAS build queues. Free builds wait in line. At peak US hours I have waited twenty minutes for a build to start. My solution is scheduling. I kick off builds before lunch. They are done after lunch. Patience is free too.&lt;/p&gt;

&lt;p&gt;Development builds are the move. Build once to your phone. Then iterate over the local network. Fast refresh works. You only hit the queue for release builds. I do maybe three release builds a week. Totally fine on free.&lt;/p&gt;

&lt;p&gt;Supabase second. The backend. Postgres database. Auth with GitHub login. Row-level security. Edge functions for webhooks. The free tier gives 500MB of database and 50,000 monthly active users. I have a tiny fraction of that. I could grow a hundred times over and still pay nothing.&lt;/p&gt;

&lt;p&gt;The catch: idle projects pause. After seven days with no activity, Supabase parks your project. The next request wakes it up. That wake-up takes a few seconds. Users see one slow load. Then everything is fast.&lt;/p&gt;

&lt;p&gt;My fix is simple. A scheduled ping. This very repo has a weekly cron in its GitHub Action. It hits the API on a schedule. The project never idles. The cron was built for publishing posts. It doubles as a keep-alive. Two birds, zero dollars.&lt;/p&gt;

&lt;p&gt;Row-level security deserves its own praise. Every query is scoped to the logged-in user at the database level. Not in my code. In Postgres itself. Even if my app code has a bug, users cannot read each other's data. Security enforced below the application layer. Free. Built in. Wonderful.&lt;/p&gt;

&lt;p&gt;Render third. Background jobs. The GitHub webhooks arrive. The AI summaries get generated. These run on Render's free tier. 750 hours a month covers one always-small service. The math works out. One service running full-time is about 720 hours. Just under the cap.&lt;/p&gt;

&lt;p&gt;The catch: cold starts. Free Render services spin down after fifteen minutes idle. The next request waits while the service boots. Ten to thirty seconds of nothing. For a background job, nobody cares. The webhook arrives, the service yawns awake, the summary generates a few seconds later. No user is watching a spinner.&lt;/p&gt;

&lt;p&gt;Do not put user-facing APIs on free Render. That is the rule. Background work only. User-facing reads go straight from the phone to Supabase. That path is always warm. Jobs can be sleepy. Users cannot.&lt;/p&gt;

&lt;p&gt;If a job ever needs to be instant, the upgrade is seven dollars. Cheapest paid tier in the stack. I have not needed it yet. When I do, one coffee a month is fine.&lt;/p&gt;

&lt;p&gt;Vercel fourth. The landing page and this blog. Static pages deploy from git pushes. SSL included. Global CDN included. 100GB of bandwidth free. My pages are tiny. I could get on the front page of everything and survive.&lt;/p&gt;

&lt;p&gt;The catch: build minutes. The free tier caps how long builds can run monthly. Static sites build in under a minute. I deploy maybe ten times a week. I use a fraction of the quota. If you add heavy image processing to builds, watch this number. I keep images pre-sized in the repo instead.&lt;/p&gt;

&lt;p&gt;The monorepo trick: my landing page and blog live with the app code. One push deploys the site. Same git history. Same workflow. Fewer dashboards to check. I check enough dashboards already.&lt;/p&gt;

&lt;p&gt;GitHub fifth. Code, CI, and project management. Unlimited public repos. 2,000 CI minutes a month on free. My publishing pipeline runs here. A few minutes a week. The budget barely notices.&lt;/p&gt;

&lt;p&gt;Everything in this pipeline is public. The app. The site. The publishing scripts. Public repos get full CI minutes and free everything. Keeping the work public is a marketing strategy that pays for its own infrastructure. Funny how that works.&lt;/p&gt;

&lt;p&gt;Now the honest section. What breaks. What I worry about. Free tiers are borrowed land. Limits change. Companies get acquired. Generosity shrinks. I have a plan for each piece.&lt;/p&gt;

&lt;p&gt;If Supabase tightens free Postgres, my database is plain Postgres. I can dump it and move anywhere. No proprietary queries. No vendor extensions. Standard SQL in, standard SQL out. Portability is the exit strategy.&lt;/p&gt;

&lt;p&gt;If Render kills free hours, background jobs move to Supabase Edge Functions. They have their own free allowance. The job code is small and stateless. It runs anywhere that runs JavaScript. Nothing is glued to Render.&lt;/p&gt;

&lt;p&gt;If Expo ever paywalls builds, the app is React Native. Ejecting is painful but possible. EAS is convenience, not lock-in. The code is mine. The config is text. I could build locally with Xcode and Android Studio. It would hurt. It would work.&lt;/p&gt;

&lt;p&gt;If Vercel caps bandwidth harder, the site is static files. Any host serves static files. Literally any. I could serve them from a free tier anywhere. Moving takes an afternoon.&lt;/p&gt;

&lt;p&gt;The principle: own your code and your data, rent the plumbing. Every service here holds nothing irreplaceable. Code lives in git. Data dumps to SQL. Nothing proprietary. I can move any piece in a day. That is what makes free tiers safe to build on.&lt;/p&gt;

&lt;p&gt;Cost so far, all time: $0. Users served: real ones. PRs reviewed from a bus: eleven. Regrets: none.&lt;/p&gt;

&lt;p&gt;Start free. Stay free until revenue says otherwise. Then spend money on exactly one thing: the bottleneck. Not before. Premature infrastructure is just a hobby that sends invoices.&lt;/p&gt;

&lt;p&gt;The demo is live. The repo is public. The pipeline that published this post cost nothing to run. That never gets old.&lt;/p&gt;

&lt;p&gt;Follow the &lt;code&gt;ok2merge-build-in-public&lt;/code&gt; series — next post Tue/Thu. Star github.com/3ni8ma/ok2merge · Try &lt;a href="https://ok2merge.vercel.app" rel="noopener noreferrer"&gt;https://ok2merge.vercel.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>indiehackers</category>
      <category>vercel</category>
      <category>supabase</category>
      <category>expo</category>
    </item>
    <item>
      <title>Expo + Supabase GitHub Auth Broke 3 Times — Here's the Fix</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Sat, 12 Sep 2026 19:38:31 +0000</pubDate>
      <link>https://dev.to/3ni8ma/expo-supabase-github-auth-broke-3-times-heres-the-fix-ijp</link>
      <guid>https://dev.to/3ni8ma/expo-supabase-github-auth-broke-3-times-heres-the-fix-ijp</guid>
      <description>&lt;p&gt;TL;DR: GitHub login in my Expo app broke three separate times. A deep link that went nowhere. A PKCE flow I wired backwards. A redirect URL with a typo. Each fix is copy-pasteable below. Total auth code is under a hundred lines. Steal it.&lt;/p&gt;

&lt;p&gt;Auth is the worst part of every app. Nobody downloads your app for the login screen. They tolerate it. Every minute you spend on OAuth is a minute nobody will ever thank you for. I spent three days on it. Here are all three failures, so you spend thirty minutes.&lt;/p&gt;

&lt;p&gt;The setup: Expo app, Supabase backend, GitHub as the login provider. Users tap "Sign in with GitHub." GitHub approves. Supabase mints a session. The app stores it. Standard stuff. Documented stuff. Stuff that still broke three times.&lt;/p&gt;

&lt;p&gt;Failure one: the deep link went nowhere.&lt;/p&gt;

&lt;p&gt;OAuth on mobile works like this. Your app opens a browser. The user approves on GitHub. GitHub redirects to a URL. That URL must reopen your app. That reopening is the deep link. If the link is wrong, the user approves your app and then stares at a browser tab. Approved. Stranded. Confused.&lt;/p&gt;

&lt;p&gt;That is exactly what happened. Login looked successful on GitHub's side. My app sat there waiting. Forever. The approve button worked. The return trip did not exist.&lt;/p&gt;

&lt;p&gt;The cause was my redirect URL. In Supabase, you register where OAuth is allowed to return. I registered the web URL of my landing page. On desktop that would be fine. On a phone, the browser does not know your app exists. The session landed on a web page. The app never heard about it.&lt;/p&gt;

&lt;p&gt;The fix: use your app scheme as the redirect. My scheme is &lt;code&gt;ok2merge://&lt;/code&gt;. So the redirect URL becomes &lt;code&gt;ok2merge://auth/callback&lt;/code&gt;. Register that exact string in two places. First, in the Supabase dashboard under Authentication, URL Configuration, Redirect URLs. Second, in your GitHub OAuth app settings as the authorization callback URL. Both must match. Character for character.&lt;/p&gt;

&lt;p&gt;In Expo, declare the scheme in your app config:&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;"expo"&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;"scheme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok2merge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ios"&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;"bundleIdentifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app.ok2merge.mobile"&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;"android"&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;"package"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app.ok2merge.mobile"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then handle the return with &lt;code&gt;expo-web-browser&lt;/code&gt; and &lt;code&gt;expo-linking&lt;/code&gt;. Open the OAuth URL in a session. Wait for the redirect. Dismiss the browser when your scheme fires:&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;WebBrowser&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;expo-web-browser&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;Linking&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;expo-linking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;WebBrowser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maybeCompleteAuthSession&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;redirectTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Linking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth/callback&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="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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signInWithOAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;redirectTo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;skipBrowserRedirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &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;url&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;WebBrowser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openAuthSessionAsync&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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;redirectTo&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;result&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Linking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&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="c1"&gt;// exchange the code for a session below&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;skipBrowserRedirect: true&lt;/code&gt; part matters. You want the OAuth URL back as data, not an automatic redirect. You open it yourself in the auth session. That way you control the return.&lt;/p&gt;

&lt;p&gt;Failure two: I wired PKCE backwards.&lt;/p&gt;

&lt;p&gt;Supabase uses PKCE for mobile OAuth. The short version: your app creates a secret verifier. It sends a challenge derived from it. GitHub holds the challenge. When the callback returns a code, your app sends the code plus the original verifier. Supabase checks they match. This proves the app that started the login is the app that finished it. No stolen codes.&lt;/p&gt;

&lt;p&gt;My bug: I generated the verifier, then threw it away. I treated the flow like web OAuth, where the server holds state. On mobile there is no server holding state. There is just your app. I launched the browser, got the code back, and tried to exchange it with no verifier. Supabase said no. Correctly.&lt;/p&gt;

&lt;p&gt;The fix is to let the Supabase client own the whole flow. Do not hand-roll the code exchange. When the deep link returns, pull the &lt;code&gt;code&lt;/code&gt; param out of the URL and call &lt;code&gt;exchangeCodeForSession&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;makeRedirectUri&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;expo-auth-session&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;params&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;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="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;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;code&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;code&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;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exchangeCodeForSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="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;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One call. The client kept the verifier in storage from &lt;code&gt;signInWithOAuth&lt;/code&gt;. It attaches it automatically. My hand-rolled exchange was the entire bug. Deleting code fixed auth. My favorite kind of fix.&lt;/p&gt;

&lt;p&gt;Lesson: never split an OAuth flow across two libraries. One library starts it, the same library finishes it. The moment two libraries share custody of a code verifier, you will lose it. I lost it. In production. In front of a user. That user was me, but still.&lt;/p&gt;

&lt;p&gt;Failure three: one dumb redirect URL typo.&lt;/p&gt;

&lt;p&gt;After fixing one and two, login worked on my phone. Then I built the Android APK. Login broke again. Same code. Same backend. Different platform, different failure. Classic.&lt;/p&gt;

&lt;p&gt;Android needs the redirect registered in its own way. The scheme works, but Android verifies app links against your package name. I had renamed the package halfway through the project. The config said one thing. The Supabase redirect list said another. Off by a suffix. A typo I introduced myself, weeks earlier, that only detonated on Android.&lt;/p&gt;

&lt;p&gt;Debugging this took an hour. The fix took ten seconds. Add the exact redirect string to Supabase. Rebuild. Done.&lt;/p&gt;

&lt;p&gt;Here is my checklist so you skip all three failures:&lt;/p&gt;

&lt;p&gt;One, use your app scheme as the redirect everywhere. &lt;code&gt;yourapp://auth/callback&lt;/code&gt;. In Supabase redirect URLs. In the GitHub OAuth app callback. In your code. All three identical.&lt;/p&gt;

&lt;p&gt;Two, one library owns the flow. &lt;code&gt;signInWithOAuth&lt;/code&gt; starts it. &lt;code&gt;exchangeCodeForSession&lt;/code&gt; finishes it. Nothing custom in between except opening the browser.&lt;/p&gt;

&lt;p&gt;Three, after renaming anything, grep the whole project for the old name. Package names. Bundle IDs. Schemes. Redirect URLs. Renames are where typos hide. They wait for the worst moment.&lt;/p&gt;

&lt;p&gt;Four, test on a real device early. The iOS simulator and Android emulator handle deep links differently from real phones. My failure one passed in the simulator. It failed on hardware. Simulators lie about links. Phones do not.&lt;/p&gt;

&lt;p&gt;Here is the full login function, all together, under a hundred lines with imports:&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;WebBrowser&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;expo-web-browser&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;Linking&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;expo-linking&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;supabase&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;./supabase&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;WebBrowser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maybeCompleteAuthSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;signInWithGitHub&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;redirectTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Linking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth/callback&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="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;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signInWithOAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;redirectTo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;skipBrowserRedirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="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;throw&lt;/span&gt; &lt;span class="nx"&gt;error&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;WebBrowser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openAuthSessionAsync&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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;redirectTo&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&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;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;result&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No auth code in callback&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;exError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exchangeCodeForSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exError&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;exError&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sign out is one line. Do not forget to test it. Untested sign-out is how you get a user permanently logged in as someone else on a shared device. Ask me how I know. Actually do not. Moving on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signOut&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auth broke three times. Each break taught me the same lesson. Mobile OAuth is a chain of exact strings. Scheme. Redirect. Package. Verifier. One wrong character anywhere breaks everything silently. The fix is always the same: line up every string, let one library hold the secrets, test on hardware.&lt;/p&gt;

&lt;p&gt;Total auth code: under a hundred lines. Total debugging: three days. Ratio is bad. Hopefully this post fixes your ratio.&lt;/p&gt;

&lt;p&gt;Next post in the series: the full $0 stack. Vercel, Render, Supabase free tiers. With a table. Everyone loves a table.&lt;/p&gt;

&lt;p&gt;Follow the &lt;code&gt;ok2merge-build-in-public&lt;/code&gt; series — next post Tue/Thu. Star github.com/3ni8ma/ok2merge · Try &lt;a href="https://ok2merge.vercel.app" rel="noopener noreferrer"&gt;https://ok2merge.vercel.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>expo</category>
      <category>supabase</category>
      <category>github</category>
      <category>reactnative</category>
    </item>
    <item>
      <title>I Built a PR Inbox for My Phone So I Stop Being the Bottleneck</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Sat, 12 Sep 2026 19:38:30 +0000</pubDate>
      <link>https://dev.to/3ni8ma/i-built-a-pr-inbox-for-my-phone-so-i-stop-being-the-bottleneck-i1l</link>
      <guid>https://dev.to/3ni8ma/i-built-a-pr-inbox-for-my-phone-so-i-stop-being-the-bottleneck-i1l</guid>
      <description>&lt;p&gt;TL;DR: I was the bottleneck on every pull request. Reviews waited days for me. So I built a PR inbox for my phone. AI summarizes each diff. I swipe right to approve. Built with Expo and Supabase. Costs $0. This is post one of the build-in-public series.&lt;/p&gt;

&lt;p&gt;I am the problem. Let me say that first.&lt;/p&gt;

&lt;p&gt;Every PR on my projects waited for me. Contributors opened good work. Then silence. For days. Not because the code was bad. Because reviewing code on a laptop feels like homework. And I kept not doing my homework.&lt;/p&gt;

&lt;p&gt;The pattern was embarrassing. Someone opens a PR on Monday. I tell myself I will review it after dinner. Dinner becomes a show. The show becomes sleep. Tuesday looks exactly like Monday. By Friday the contributor has moved on with their life. The PR has merge conflicts. Everyone loses.&lt;/p&gt;

&lt;p&gt;I tried to fix this with discipline. That lasted four days. Discipline is a terrible dependency. It has no SLA. It goes down on weekends.&lt;/p&gt;

&lt;p&gt;So I fixed it with software instead. I built a PR inbox for my phone. The idea is stupidly simple. Your email has an inbox. Your texts have an inbox. Your code reviews deserve one too. Open the app. See every PR waiting on you. Swipe right to approve. Swipe left to request changes. Done from the couch.&lt;/p&gt;

&lt;p&gt;The trick is the summary. Nobody wants to read a raw diff on a phone. Raw diffs on a five-inch screen are a punishment. So each PR gets a plain-language summary. What changed. Why it changed. What could break. You read three sentences instead of three hundred lines. Then you decide.&lt;/p&gt;

&lt;p&gt;AI writes the summary. You make the call. That division of labor matters to me. The model explains. The human decides. I never wanted auto-merge. Auto-merge is how you deploy a disaster at 2am. I wanted faster understanding, not zero judgment.&lt;/p&gt;

&lt;p&gt;Here is how a review goes now. Phone buzzes. New PR from a contributor. I open it while waiting for coffee. The summary says it refactors the auth callback and touches two files. Risk looks low. I glance at the actual diff for the risky hunk. Looks fine. Swipe right. Merged before my coffee is ready.&lt;/p&gt;

&lt;p&gt;Total time: ninety seconds. Old way: three days of guilt plus twenty minutes of context-switching. This is not a small improvement. This is a different life.&lt;/p&gt;

&lt;p&gt;Let me talk about the stack. Because the stack is the other point of this post. The whole thing costs zero dollars. Expo for the app. Supabase for auth and database. GitHub webhooks for PR events. Vercel for the landing page. Every tier is free. My monthly infrastructure bill is the price of nothing.&lt;/p&gt;

&lt;p&gt;Expo gets mocked a lot. I used to mock it. Then I shipped with it. Over-the-air updates alone are worth it. I push a fix and users get it without an app store review. When your QA team is you, that speed is everything.&lt;/p&gt;

&lt;p&gt;Supabase handles the boring parts. Auth. Postgres. Row-level security. I did not want to run a server. I have run servers before. They page you at night. Supabase pages someone else. That someone else is not me. Perfect arrangement.&lt;/p&gt;

&lt;p&gt;The architecture is boring on purpose. GitHub sends a webhook when a PR opens. A small function stores the PR metadata in Supabase. Another function asks a model to summarize the diff. The phone app reads from Supabase and shows the inbox. Approve or request-changes goes back through the GitHub API. No magic. Just plumbing.&lt;/p&gt;

&lt;p&gt;Boring architecture is a feature. Every clever system I have ever built became a second job. This one has been running for weeks. I barely think about it. It just buzzes when someone needs me.&lt;/p&gt;

&lt;p&gt;The hardest part was not the code. It was the summary quality. Early summaries were useless. They said things like "this PR modifies code to improve functionality." Thanks. Very helpful. I could have guessed that.&lt;/p&gt;

&lt;p&gt;I fixed it with structure. The summarizer now answers four questions. What changed. Why it probably changed. Riskiest hunk. What to check before approving. Forcing that shape killed the vague filler. A summary that names the riskiest file is worth ten generic ones.&lt;/p&gt;

&lt;p&gt;The second hardest part was auth. GitHub OAuth on mobile broke three times. That story is its own post. It is literally the next post in this series. Spoiler: deep links, PKCE, and one very dumb redirect URL.&lt;/p&gt;

&lt;p&gt;Let me share the numbers. Since I started using the inbox, my median review time went from three days to under four hours. Most of that is still me being slow. The app did its part instantly. I merged eleven PRs from a bus. I merged one from a wedding. Do not tell the bride.&lt;/p&gt;

&lt;p&gt;Contributors noticed. One told me reviews got "weirdly fast." That is the best review my review tool ever got. Weirdly fast. I want that on a sticker.&lt;/p&gt;

&lt;p&gt;There is a real lesson here. Bottlenecks are usually interface problems. I did not review PRs because the interface for reviewing was a laptop, a quiet room, and forty free minutes. I never have all three at once. The phone interface needs ninety seconds and a couch. Same human. Same brain. Different friction.&lt;/p&gt;

&lt;p&gt;Find your friction. That is the whole post, really. Where is the task you keep avoiding? What does it demand from you? A desk? An hour? Silence? Shrink the demand. The motivation was never the problem. The setup cost was.&lt;/p&gt;

&lt;p&gt;What is next. The inbox works for my repos. Now I am opening it up. Multi-repo support. Team inboxes. Smarter risk scoring. A demo is live at the link below. Star the repo if you want to follow along. Real code, real bugs, real numbers. Every Tuesday and Thursday.&lt;/p&gt;

&lt;p&gt;I built this so I stop being the bottleneck. It worked. Now help me break it in new and interesting ways.&lt;/p&gt;

&lt;p&gt;Follow the &lt;code&gt;ok2merge-build-in-public&lt;/code&gt; series — next post Tue/Thu. Star github.com/3ni8ma/ok2merge · Try &lt;a href="https://ok2merge.vercel.app" rel="noopener noreferrer"&gt;https://ok2merge.vercel.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>mobile</category>
      <category>reactnative</category>
      <category>ai</category>
    </item>
    <item>
      <title>Algorithmic Trading Signals: From Indicators to Backtests</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Tue, 08 Sep 2026 16:51:48 +0000</pubDate>
      <link>https://dev.to/3ni8ma/algorithmic-trading-signals-from-indicators-to-backtests-173j</link>
      <guid>https://dev.to/3ni8ma/algorithmic-trading-signals-from-indicators-to-backtests-173j</guid>
      <description>&lt;h2&gt;
  
  
  The Signal Vocabulary
&lt;/h2&gt;

&lt;p&gt;The core indicators: RSI (14-period, 70/30 thresholds) for momentum exhaustion, MACD (12/26/9) for trend shifts, and moving-average crossovers for regime changes. Each is a lagging function of price — the signal fires after the move starts. Combining uncorrelated signals is the start of an edge; stacking correlated ones is just noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signal Construction, Not Indicator Collection
&lt;/h2&gt;

&lt;p&gt;A strategy is a decision rule: entry conditions, exit conditions, position sizing. The discipline: define signals as boolean predicates over indicator state, and define exits as first-class (stop-loss, take-profit, time-based) rather than as afterthoughts. A strategy without explicit exits is a strategy without risk control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backtesting Without Lying to Yourself
&lt;/h2&gt;

&lt;p&gt;The backtest mechanics that matter: no lookahead (indicators must use only data available at decision time), realistic slippage and fees, and bar-close execution instead of magic fills. The classic failure: a backtest that assumes you bought at the intraday low — every strategy wins on paper with hindsight fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overfitting and the Parameter Lottery
&lt;/h2&gt;

&lt;p&gt;Grid-searching 200 parameter combinations on one ticker and reporting the best produces a strategy optimized for history, not markets. The guardrails: walk-forward validation (rolling train/test windows), out-of-sample holdouts, and parameter stability — a strategy whose edge vanishes when RSI(12) becomes RSI(14) has no edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Backtest to Paper Trading
&lt;/h2&gt;

&lt;p&gt;The gap between backtest and live is regime change. Paper trading the strategy against live data for weeks reveals execution reality: fills, latency, data gaps. The promotion criteria: paper performance within a tolerance of backtest expectations, and behavior in a down-market window.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Limits
&lt;/h2&gt;

&lt;p&gt;Retail quant trading is a statistics exercise against professionals with better data and execution. The realistic goals: disciplined exits, systematic entries, and risk management that caps losses — an edge that compounds slowly beats a lottery ticket. Treat the strategy as a system to operate, not a prediction to trust.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The bank-driven fallback wrote this post because the LLM proxy was unreachable — structure and facts come from the topic outline, and the next regeneration will enrich it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Signal Vocabulary&lt;/li&gt;
&lt;li&gt;Signal Construction, Not Indicator Collection&lt;/li&gt;
&lt;li&gt;Backtesting Without Lying to Yourself&lt;/li&gt;
&lt;li&gt;Overfitting and the Parameter Lottery&lt;/li&gt;
&lt;li&gt;From Backtest to Paper Trading&lt;/li&gt;
&lt;li&gt;The Honest Limits&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in the signal vocabulary?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in signal construction, not indicator collection?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in backtesting without lying to yourself?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

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

&lt;p&gt;A trading signal is only as good as the discipline around it: explicit exits, honest backtests, walk-forward validation, and paper trading before capital. The system — not the indicator — is the product, and its edge must survive the test of time out of sample.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/AutoHedge" rel="noopener noreferrer"&gt;View the project on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>trading</category>
      <category>quant</category>
      <category>backtesting</category>
      <category>indicators</category>
    </item>
    <item>
      <title>I Built a Microservice in 10 Minutes with GoFr (and Didn't Touch YAML)</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Sat, 05 Sep 2026 20:36:59 +0000</pubDate>
      <link>https://dev.to/3ni8ma/i-built-a-microservice-in-10-minutes-with-gofr-and-didnt-touch-yaml-2f28</link>
      <guid>https://dev.to/3ni8ma/i-built-a-microservice-in-10-minutes-with-gofr-and-didnt-touch-yaml-2f28</guid>
      <description>&lt;p&gt;I've built enough Express and Flask APIs to know the drill: router, DB client, logger, metrics, health endpoint, Dockerfile, then three hours of glue. I wanted to see if Go's GoFr framework actually removes that.&lt;/p&gt;

&lt;p&gt;Short answer: mostly yes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is GoFr?
&lt;/h2&gt;

&lt;p&gt;GoFr (&lt;a href="https://gofr.dev" rel="noopener noreferrer"&gt;https://gofr.dev&lt;/a&gt;) is an opinionated microservice framework for Go. CNCF-listed, 20k+ stars. The pitch: REST standards by default, config management, auth middleware, DB migrations, cron, websockets, Swagger, plus logs/traces/metrics out of the box. K8s-first thinking.&lt;/p&gt;

&lt;p&gt;I liked that it doesn't try to be minimal. It tries to be complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hello World in 5 lines
&lt;/h2&gt;

&lt;p&gt;Prereq: Go 1.24+.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get &lt;span class="nt"&gt;-u&lt;/span&gt; gofr.dev/pkg/gofr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;main.go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"gofr.dev/pkg/gofr"&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gofr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/greet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gofr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&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="s"&gt;"Hello World!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c"&gt;// localhost:8000&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;go run main.go&lt;/code&gt;, open &lt;code&gt;localhost:8000/greet&lt;/code&gt;. Done. No router setup, no server boilerplate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me: observability
&lt;/h2&gt;

&lt;p&gt;Terminal logs are pretty-printed with units. JSON logs have the same fields but no time unit on &lt;code&gt;response_time&lt;/code&gt; — I hit exactly what issue #2480 discusses. Metrics buckets cap at 30s, so anything above shows as 30s. It's a small papercut, but it shows the project cares about log parity.&lt;/p&gt;

&lt;p&gt;Health checks for datasources, built-in tracing, and Swagger rendering are all one-liners in the docs. I didn't need Prometheus client code for basic RED metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next: porting my real project
&lt;/h2&gt;

&lt;p&gt;I maintain &lt;code&gt;astro-tasks&lt;/code&gt; — a Python CLI dev dashboard on PyPI with GitHub CLI integration and git health checks — and &lt;code&gt;Finance-Hub&lt;/code&gt; (live market monitoring + AI predictions).&lt;/p&gt;

&lt;p&gt;Both currently poll APIs on a loop and I hand-roll logging. Next step: port Finance-Hub's market poller to GoFr:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /markets&lt;/code&gt; with built-in observability instead of my Python logger&lt;/li&gt;
&lt;li&gt;GoFr cron for the polling interval instead of &lt;code&gt;while True + sleep&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Health check for the data source instead of my custom check&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same for my Rust static file server in &lt;code&gt;rust-projects&lt;/code&gt; — GoFr's abstracted file systems + Swagger would kill half that code.&lt;/p&gt;

&lt;p&gt;If you're a Python/TS dev curious about Go, GoFr is the lowest-friction entry I've found. No YAML hell, just code.&lt;/p&gt;

&lt;p&gt;What would you build? I'm thinking URL shortener next.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built with GoFr, Go 1.24. Repo: &lt;a href="https://github.com/gofr-dev/gofr" rel="noopener noreferrer"&gt;https://github.com/gofr-dev/gofr&lt;/a&gt; Docs: &lt;a href="https://gofr.dev/docs" rel="noopener noreferrer"&gt;https://gofr.dev/docs&lt;/a&gt; My projects: &lt;a href="https://github.com/3ni8ma" rel="noopener noreferrer"&gt;https://github.com/3ni8ma&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>go</category>
      <category>gofr</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Design Tokens and Theme Systems with Tailwind</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Sat, 05 Sep 2026 16:41:07 +0000</pubDate>
      <link>https://dev.to/3ni8ma/design-tokens-and-theme-systems-with-tailwind-i2i</link>
      <guid>https://dev.to/3ni8ma/design-tokens-and-theme-systems-with-tailwind-i2i</guid>
      <description>&lt;h2&gt;
  
  
  Three Layers of Tokens
&lt;/h2&gt;

&lt;p&gt;Primitive tokens are the raw materials (a color ramp, a type scale); semantic tokens map to intent (background, surface, text-accent); component tokens bind semantics to components (button-bg, card-border). The rule that keeps themes sane: components reference semantic tokens only — never primitives directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSS Variables as the Runtime
&lt;/h2&gt;

&lt;p&gt;Tokens as CSS custom properties on &lt;code&gt;:root&lt;/code&gt;, overridden by scoped selectors. Theme switching via &lt;code&gt;data-theme&lt;/code&gt; attributes: &lt;code&gt;html[data-theme='dark'] { --surface: ... }&lt;/code&gt;. The tradeoff: CSS variables resolve at runtime (instant theming, no rebuild) at the cost of not being static values.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pitfalls of Variables in Gradients
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;var(--x)&lt;/code&gt; inside &lt;code&gt;radial-gradient()&lt;/code&gt; stops and some &lt;code&gt;background-image&lt;/code&gt; url() contexts fails to resolve in several browsers. The workarounds: hardcoded values in gradient stops, or &lt;code&gt;html[data-mode]&lt;/code&gt; selector overrides that swap entire gradient declarations instead of referencing variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dark Mode Without Duplication
&lt;/h2&gt;

&lt;p&gt;The mode-agnostic design: semantic tokens stay the same; only surface-layer tokens flip with &lt;code&gt;data-mode&lt;/code&gt;. Body backgrounds, text colors, and glass effects swap; accent palettes remain identical across modes. One component stylesheet, two modes, zero &lt;code&gt;dark:&lt;/code&gt; prefixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending Tailwind with a Plugin
&lt;/h2&gt;

&lt;p&gt;A Tailwind plugin adds utilities and theme values from your tokens: &lt;code&gt;theme.extend.colors&lt;/code&gt; from semantic tokens, custom utilities via &lt;code&gt;addUtilities&lt;/code&gt;, and component classes via &lt;code&gt;addComponents&lt;/code&gt;. The payoff: &lt;code&gt;bg-surface&lt;/code&gt;, &lt;code&gt;text-accent&lt;/code&gt;, &lt;code&gt;glass&lt;/code&gt; — your design language becomes the utility vocabulary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping a Token Package
&lt;/h2&gt;

&lt;p&gt;Tokens as code: a TypeScript/JSON source of truth, build scripts generating the CSS variables and the Tailwind config, and versioning for consumers. A token change propagates to every consumer through the build — the theme is a dependency, not a find-and-replace.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The bank-driven fallback wrote this post because the LLM proxy was unreachable — structure and facts come from the topic outline, and the next regeneration will enrich it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Three Layers of Tokens&lt;/li&gt;
&lt;li&gt;CSS Variables as the Runtime&lt;/li&gt;
&lt;li&gt;The Pitfalls of Variables in Gradients&lt;/li&gt;
&lt;li&gt;Dark Mode Without Duplication&lt;/li&gt;
&lt;li&gt;Extending Tailwind with a Plugin&lt;/li&gt;
&lt;li&gt;Shipping a Token Package&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in three layers of tokens?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in css variables as the runtime?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in the pitfalls of variables in gradients?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

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

&lt;p&gt;A theme system is architecture: primitive, semantic, and component layers that keep design intent stable while visuals evolve. With CSS variables as the runtime and Tailwind as the vocabulary, a redesign becomes a token update instead of a rewrite.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/tailwind-plugin" rel="noopener noreferrer"&gt;View the project on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>designtokens</category>
      <category>tailwindcss</category>
      <category>css</category>
      <category>theming</category>
    </item>
    <item>
      <title>Crawling at Scale: Scheduling, Deduplication, and Storage</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Thu, 03 Sep 2026 16:01:48 +0000</pubDate>
      <link>https://dev.to/3ni8ma/crawling-at-scale-scheduling-deduplication-and-storage-3e5p</link>
      <guid>https://dev.to/3ni8ma/crawling-at-scale-scheduling-deduplication-and-storage-3e5p</guid>
      <description>&lt;h2&gt;
  
  
  The Frontier: What to Fetch Next
&lt;/h2&gt;

&lt;p&gt;The crawl frontier is a priority queue of URLs, not a set. Rules that matter: scope filtering (stay on allowed hosts/paths), freshness (when to re-fetch a page), and prioritization (document pages over index pages). A frontier without priorities degenerates into hammering the same section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Politeness as a Hard Requirement
&lt;/h2&gt;

&lt;p&gt;The politeness policy: delay between requests per host (configurable, generous defaults), retry with exponential backoff on 429/5xx, and &lt;code&gt;Last-Modified&lt;/code&gt;/ETag conditional fetches. A crawler that respects politeness is tolerated; one that doesn't gets blocked — and burns the IP for everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content Deduplication
&lt;/h2&gt;

&lt;p&gt;Crawled pages are full of near-duplicates: pagination, boilerplate, template variants. The patterns: canonical URL detection, normalized-content hashing (strip whitespace and boilerplate before hashing), and similarity thresholds for near-dupes. Dedup before storage — every duplicate stored is noise in the dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incremental Re-Crawl
&lt;/h2&gt;

&lt;p&gt;Re-crawling everything every time is wasteful. The design: a crawl manifest keyed by URL with last-fetched time and content hash; re-crawl decisions based on freshness TTLs and change signals (headers, sitemap modtime). The dataset stays current at a fraction of the full-crawl cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage: Raw, Extracted, Derived
&lt;/h2&gt;

&lt;p&gt;The three-layer store: raw HTML in object storage (cheap, immutable), extracted content in a queryable database (documents, metadata, full text), and derived artifacts (indexes, embeddings) in purpose-built stores. The pipeline is idempotent at every layer — re-running a stage must never corrupt the layers below.&lt;/p&gt;

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

&lt;p&gt;A crawler as a service: scheduler (cron or queue) → fetcher workers (bounded concurrency per host) → extractor → dedup → storage. Observability: per-host stats, error rates, and crawl coverage reports. A crawler you can't see into is a crawler you can't trust.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The bank-driven fallback wrote this post because the LLM proxy was unreachable — structure and facts come from the topic outline, and the next regeneration will enrich it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Frontier: What to Fetch Next&lt;/li&gt;
&lt;li&gt;Politeness as a Hard Requirement&lt;/li&gt;
&lt;li&gt;Content Deduplication&lt;/li&gt;
&lt;li&gt;Incremental Re-Crawl&lt;/li&gt;
&lt;li&gt;Storage: Raw, Extracted, Derived&lt;/li&gt;
&lt;li&gt;The System Architecture&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in the frontier: what to fetch next?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in politeness as a hard requirement?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in content deduplication?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

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

&lt;p&gt;Scaling a crawler is an exercise in discipline: priorities, politeness, deduplication, and storage separation. The pipeline that respects those four constraints scales to thousands of pages without drama — and produces a dataset you can actually trust.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/DoxDock" rel="noopener noreferrer"&gt;View the project on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webcrawling</category>
      <category>scraping</category>
      <category>pipelines</category>
      <category>deduplication</category>
    </item>
    <item>
      <title>GitHub Automation with the gh CLI: Triage, PRs, and Workflows</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Tue, 01 Sep 2026 16:38:29 +0000</pubDate>
      <link>https://dev.to/3ni8ma/github-automation-with-the-gh-cli-triage-prs-and-workflows-297h</link>
      <guid>https://dev.to/3ni8ma/github-automation-with-the-gh-cli-triage-prs-and-workflows-297h</guid>
      <description>&lt;h2&gt;
  
  
  gh as the Complete API Client
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gh api&lt;/code&gt; exposes the entire REST and GraphQL surface with auth built in. The pattern that unlocks everything: &lt;code&gt;gh api repos/{owner}/{repo}/issues&lt;/code&gt; piped to jq, and &lt;code&gt;gh run watch&lt;/code&gt; for workflow observability. The CLI is a thin wrapper over the API — everything the website can do, a script can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated Issue Triage
&lt;/h2&gt;

&lt;p&gt;The triage loop: fetch unlabeled issues, classify by keywords and titles, apply labels, assign priorities, and route to the right maintainer. A nightly script keeps a backlog organized without anyone touching the web UI. The discipline: classification must be conservative — an untriaged issue is better than a wrong label.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR Review Automation
&lt;/h2&gt;

&lt;p&gt;The reviewer's checklist as code: check CI status, verify the diff touches only declared files, confirm the description matches the change, and comment when something fails the check. &lt;code&gt;gh pr view --json&lt;/code&gt; + &lt;code&gt;gh pr diff&lt;/code&gt; make the review scriptable; the script comments, a human decides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow Lifecycle Management
&lt;/h2&gt;

&lt;p&gt;Beyond triggering: &lt;code&gt;gh workflow disable&lt;/code&gt; for flaky jobs, &lt;code&gt;gh run rerun&lt;/code&gt; for transient failures, and &lt;code&gt;gh api&lt;/code&gt; to approve protected-branch runs. The automation pattern: a cron that checks for failed runs older than a threshold and either reruns them once or files an issue — flaky CI stops being noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-Repo Coordination
&lt;/h2&gt;

&lt;p&gt;One script, many repos: a loop over &lt;code&gt;gh repo list&lt;/code&gt; runs the same triage or release check everywhere, and &lt;code&gt;gh issue create --repo&lt;/code&gt; files findings in each. For the 3-4 repos one person actually maintains, cross-repo automation collapses hours of manual clicking into one scheduled pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identity and Safety
&lt;/h2&gt;

&lt;p&gt;Automated actions run as a bot user or a PAT — the audit trail must show who did what. The safety patterns: &lt;code&gt;--dry-run&lt;/code&gt; flags everywhere, confirmation prompts for destructive ops (deletes, force-pushes, label removal), and rate-limit awareness (&lt;code&gt;gh api rate_limit&lt;/code&gt;).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The bank-driven fallback wrote this post because the LLM proxy was unreachable — structure and facts come from the topic outline, and the next regeneration will enrich it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;gh as the Complete API Client&lt;/li&gt;
&lt;li&gt;Automated Issue Triage&lt;/li&gt;
&lt;li&gt;PR Review Automation&lt;/li&gt;
&lt;li&gt;Workflow Lifecycle Management&lt;/li&gt;
&lt;li&gt;Cross-Repo Coordination&lt;/li&gt;
&lt;li&gt;Identity and Safety&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in gh as the complete api client?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in automated issue triage?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in pr review automation?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

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

&lt;p&gt;The gh CLI makes GitHub a scripting surface: triage, review checks, workflow hygiene, and cross-repo maintenance all become scheduled, testable, and reviewable. The discipline — conservative classification, dry runs, honest audit trails — is what keeps the automation trusted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/3ni8ma" rel="noopener noreferrer"&gt;View the project on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>cli</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>Running a Local LLM Proxy: OpenAI-Compatible Gateways</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Sat, 29 Aug 2026 16:01:46 +0000</pubDate>
      <link>https://dev.to/3ni8ma/running-a-local-llm-proxy-openai-compatible-gateways-2ka8</link>
      <guid>https://dev.to/3ni8ma/running-a-local-llm-proxy-openai-compatible-gateways-2ka8</guid>
      <description>&lt;h2&gt;
  
  
  Why a Gateway
&lt;/h2&gt;

&lt;p&gt;Every AI tool — editors, agents, scripts — speaks the OpenAI chat-completions dialect. A local gateway that speaks that dialect and forwards to whatever model you actually run makes every tool plug into local inference with zero code changes. One port, many consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compatibility Contract
&lt;/h2&gt;

&lt;p&gt;The surface that must match: &lt;code&gt;/v1/chat/completions&lt;/code&gt; with &lt;code&gt;messages&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;max_tokens&lt;/code&gt;; SSE streaming via &lt;code&gt;stream: true&lt;/code&gt;; and error responses shaped like OpenAI's. Tools that validate models (a hardcoded model list) will reject your endpoint — accept any model name and map it, or report it honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming Without Breaking Clients
&lt;/h2&gt;

&lt;p&gt;Clients that expect streaming fail on buffered responses. The implementation: read the upstream stream chunk-by-chunk, re-emit as SSE &lt;code&gt;data:&lt;/code&gt; frames, flush on each token, and terminate with &lt;code&gt;data: [DONE]&lt;/code&gt;. Timeouts are the silent killer — an idle upstream must send keep-alive comments or the client hangs forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching as the Cost Killer
&lt;/h2&gt;

&lt;p&gt;Identical prompts are common across sessions (system prompts, templates, retries). A prompt-hash cache with TTL serves repeated requests instantly and cuts provider spend. The design decision: cache exact matches only — semantic caching of LLM output is where correctness dies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing and Fallback
&lt;/h2&gt;

&lt;p&gt;A gateway that routes by model name, cost budget, or availability. The fallback ladder: local model → free tier → paid provider, with health checks that demote a failing upstream. The pattern: retry with backoff on 429/5xx, fail over on repeated failures, and never silently return a different model than requested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational Details That Matter
&lt;/h2&gt;

&lt;p&gt;launchd or systemd supervision, structured logs with per-request latency, and a health endpoint (&lt;code&gt;/v1/models&lt;/code&gt;) for uptime monitoring. The gateway becomes the single place where model behavior — version, temperature caps, token limits — is controlled for the whole machine.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The bank-driven fallback wrote this post because the LLM proxy was unreachable — structure and facts come from the topic outline, and the next regeneration will enrich it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Why a Gateway&lt;/li&gt;
&lt;li&gt;The Compatibility Contract&lt;/li&gt;
&lt;li&gt;Streaming Without Breaking Clients&lt;/li&gt;
&lt;li&gt;Caching as the Cost Killer&lt;/li&gt;
&lt;li&gt;Routing and Fallback&lt;/li&gt;
&lt;li&gt;Operational Details That Matter&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in why a gateway?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in the compatibility contract?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in streaming without breaking clients?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

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

&lt;p&gt;A local LLM proxy is the quiet infrastructure that makes local AI practical: one compatible endpoint, streaming done right, caching for cost, and honest routing. It turns 'AI tools need APIs' into 'everything already works, it's just local now.'&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/deepseek-free-proxy" rel="noopener noreferrer"&gt;View the project on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>api</category>
      <category>proxy</category>
      <category>localai</category>
    </item>
    <item>
      <title>Vite Plugin Development: Hooks, Transforms, and Virtual Modules</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Thu, 27 Aug 2026 16:01:55 +0000</pubDate>
      <link>https://dev.to/3ni8ma/vite-plugin-development-hooks-transforms-and-virtual-modules-4n7h</link>
      <guid>https://dev.to/3ni8ma/vite-plugin-development-hooks-transforms-and-virtual-modules-4n7h</guid>
      <description>&lt;h2&gt;
  
  
  The Plugin Container Model
&lt;/h2&gt;

&lt;p&gt;A Vite plugin is an object with hooks, layered over Rollup. The phases: &lt;code&gt;config&lt;/code&gt; (adjust Vite config), &lt;code&gt;configResolved&lt;/code&gt; (read final config), &lt;code&gt;buildStart&lt;/code&gt; (initialize), &lt;code&gt;transform&lt;/code&gt; (rewrite module source), &lt;code&gt;generateBundle&lt;/code&gt; (write output files), &lt;code&gt;closeBundle&lt;/code&gt;. Understanding when each runs — dev vs build — is half the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transforms: Rewriting Modules Safely
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;transform&lt;/code&gt; hook receives code + id and returns modified code. The discipline: only touch files you own (check &lt;code&gt;id&lt;/code&gt; patterns), return &lt;code&gt;null&lt;/code&gt; to pass through untouched, and never rely on textual assumptions about frameworks — use the loaders' ASTs or the module graph instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Virtual Modules for Injected Code
&lt;/h2&gt;

&lt;p&gt;Virtual modules let a plugin provide modules that don't exist on disk: &lt;code&gt;virtual:my-plugin-data&lt;/code&gt;. Intercept in &lt;code&gt;resolveId&lt;/code&gt;, emit in &lt;code&gt;load&lt;/code&gt;. This is how plugins inject runtime data (config, asset manifests, environment) without touching the user's source tree — and how sitemap plugins feed route lists to components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build-Time Asset Generation
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;generateBundle&lt;/code&gt; hook writes files into the output: sitemaps, robots, feeds, OG images. The pattern: collect data during earlier hooks (routes from the module graph, page metadata from transforms), then emit &lt;code&gt;this.emitFile({ type: 'asset', fileName, source })&lt;/code&gt;. Assets emitted here are hashed, deployed, and cacheable like any other build output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dev Mode: Middleware and HMR
&lt;/h2&gt;

&lt;p&gt;In dev, plugins get an HTTP middleware (&lt;code&gt;configureServer&lt;/code&gt;) and HMR hooks. A sitemap plugin doesn't need it, but a plugin serving mock APIs or proxying websockets lives here. The rule: dev features must not leak into the production bundle — gate everything on &lt;code&gt;config.command&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing and Testing Plugins
&lt;/h2&gt;

&lt;p&gt;A Vite plugin is a library: &lt;code&gt;vite-plugin-*&lt;/code&gt; naming, ESM-first, exports map, and a test suite that runs the real Vite build against a fixture project. The &lt;code&gt;buildTest&lt;/code&gt; pattern: a temp project, &lt;code&gt;build()&lt;/code&gt;, and assertions on output files — faster and more honest than unit-testing hooks in isolation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The bank-driven fallback wrote this post because the LLM proxy was unreachable — structure and facts come from the topic outline, and the next regeneration will enrich it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Plugin Container Model&lt;/li&gt;
&lt;li&gt;Transforms: Rewriting Modules Safely&lt;/li&gt;
&lt;li&gt;Virtual Modules for Injected Code&lt;/li&gt;
&lt;li&gt;Build-Time Asset Generation&lt;/li&gt;
&lt;li&gt;Dev Mode: Middleware and HMR&lt;/li&gt;
&lt;li&gt;Publishing and Testing Plugins&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in the plugin container model?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in transforms: rewriting modules safely?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in virtual modules for injected code?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

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

&lt;p&gt;Vite plugins are the cleanest expression of the bundler-as-platform idea. Hook order, transform discipline, and virtual modules cover most real needs — and a plugin that generates build artifacts (sitemaps, feeds) is one of the highest-leverage integrations a site can have.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/vite-plugin" rel="noopener noreferrer"&gt;View the project on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>vite</category>
      <category>plugins</category>
      <category>buildtools</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Browser Automation That Looks Human</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Tue, 25 Aug 2026 16:02:07 +0000</pubDate>
      <link>https://dev.to/3ni8ma/browser-automation-that-looks-human-4m94</link>
      <guid>https://dev.to/3ni8ma/browser-automation-that-looks-human-4m94</guid>
      <description>&lt;h2&gt;
  
  
  What Detection Systems Measure
&lt;/h2&gt;

&lt;p&gt;A fingerprint is a vector: user agent, WebGL vendor and renderer strings, canvas hash, audio context, screen metrics, fonts, timezone, and navigator property consistency. Bot detectors score the vector for anomalies — a headless Chromium with &lt;code&gt;navigator.webdriver=true&lt;/code&gt; fails instantly; the game is scoring like a real device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patching the Leaky Surface
&lt;/h2&gt;

&lt;p&gt;The leaks: &lt;code&gt;navigator.webdriver&lt;/code&gt;, missing &lt;code&gt;window.chrome&lt;/code&gt;, the &lt;code&gt;--headless&lt;/code&gt; UA, and iframe parent attributes. The fixes: CDP session to override properties before any script runs, spoofed WebGL strings, and a realistic UA/fingerprint pair. The rule: every override must be consistent — a Chrome UA with a Safari canvas hash is worse than no spoofing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Input That Reads as Human
&lt;/h2&gt;

&lt;p&gt;Automated typing is instant and perfectly regular; humans type with variable inter-key latency, occasional corrections, and pauses. The pattern: randomized per-character delays with log-normal distribution, jittered mouse movement along bezier curves, and scroll speeds that vary. Timing consistency across a session matters more than any single delay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session Persistence: The Cookie Jar That Matters
&lt;/h2&gt;

&lt;p&gt;Sites trust continuity: a fresh context with no cookies, no localStorage, and a first-seen IP screams automation. The pattern: persistent browser profiles stored between runs, realistic storage state, and reusing the same context for related tasks. Trust compounds over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Orchestrating Multi-Context Automation
&lt;/h2&gt;

&lt;p&gt;Different tasks need different identities. The architecture: a context manager that provisions profiles, health-checks them (does this context still pass a fingerprint test?), and rotates on failure. A context that gets flagged is retired, not reused — one burned context can poison everything it touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ethics of Blending In
&lt;/h2&gt;

&lt;p&gt;Human-like automation exists on a spectrum: scraping public data with throttling is defensible; impersonating a user or evading explicit blocks is not. The operational rules that keep a project defensible: rate limits well under human capacity, respect robots.txt and ToS, and never automate account creation or payment flows.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The bank-driven fallback wrote this post because the LLM proxy was unreachable — structure and facts come from the topic outline, and the next regeneration will enrich it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;What Detection Systems Measure&lt;/li&gt;
&lt;li&gt;Patching the Leaky Surface&lt;/li&gt;
&lt;li&gt;Input That Reads as Human&lt;/li&gt;
&lt;li&gt;Session Persistence: The Cookie Jar That Matters&lt;/li&gt;
&lt;li&gt;Orchestrating Multi-Context Automation&lt;/li&gt;
&lt;li&gt;The Ethics of Blending In&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in what detection systems measure?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in patching the leaky surface?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in input that reads as human?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

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

&lt;p&gt;Human-like browser automation is a fingerprint consistency problem, not a feature list. Every patched leak must be coherent with the identity you're projecting — and the engineering discipline matters less than the boundaries you refuse to cross.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/anomalyco/camofox-browser" rel="noopener noreferrer"&gt;View the project on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>browserautomation</category>
      <category>playwright</category>
      <category>stealth</category>
      <category>fingerprinting</category>
    </item>
  </channel>
</rss>
