<?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: siyadhkc</title>
    <description>The latest articles on DEV Community by siyadhkc (@siyadhkc).</description>
    <link>https://dev.to/siyadhkc</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%2F3868439%2F5c0b2e2e-0a7c-406f-9d99-1d810624972c.png</url>
      <title>DEV Community: siyadhkc</title>
      <link>https://dev.to/siyadhkc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/siyadhkc"/>
    <language>en</language>
    <item>
      <title>Why Most Django Boilerplates Are Insecure by Default</title>
      <dc:creator>siyadhkc</dc:creator>
      <pubDate>Wed, 03 Jun 2026 16:18:06 +0000</pubDate>
      <link>https://dev.to/siyadhkc/why-most-django-boilerplates-are-insecure-by-default-4nki</link>
      <guid>https://dev.to/siyadhkc/why-most-django-boilerplates-are-insecure-by-default-4nki</guid>
      <description>&lt;p&gt;You found a Django boilerplate on GitHub. It has 2k stars, a clean README, Docker support, and a Makefile. You clone it, rename a few things, push to production.&lt;/p&gt;

&lt;p&gt;Congratulations. You just shipped someone else's security assumptions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Illusion of "Production-Ready"
&lt;/h2&gt;

&lt;p&gt;Every boilerplate claims to be production-ready. The badge is right there in the README. But production-ready for what? A side project? A fintech API? A platform that stores user PII?&lt;/p&gt;

&lt;p&gt;"Production-ready" in most boilerplates means: &lt;em&gt;it runs without errors&lt;/em&gt;. That's it. The security part is usually left as a comment that says &lt;code&gt;# TODO: change this before deploy&lt;/code&gt; — which, statistically, nobody changes.&lt;/p&gt;

&lt;p&gt;This isn't an attack on boilerplate authors. They're solving a real problem: Django's initial setup is tedious, and abstracting that away has genuine value. The problem is that the &lt;em&gt;defaults they choose&lt;/em&gt; get inherited silently by every project that forks them.&lt;/p&gt;

&lt;p&gt;And defaults are everything in security.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Ships in Most Boilerplates
&lt;/h2&gt;

&lt;p&gt;Let me walk through what you'll typically find if you dig past the README.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;DEBUG = True&lt;/code&gt; Tied to an Environment Variable That Defaults to True
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
&lt;/span&gt;&lt;span class="n"&gt;DEBUG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;True&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;True&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the canonical mistake. The intent is good — make DEBUG configurable. But the &lt;em&gt;default&lt;/em&gt; is True. So if you deploy and forget to set the environment variable, Django will cheerfully expose your full stack trace, local variables, installed apps, and settings to anyone who hits a 404.&lt;/p&gt;

&lt;p&gt;I've seen this in repos with thousands of stars. The author probably never intended it to ship this way. But it does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is trivial:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DEBUG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;False&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;True&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One word. The default should always be the safe option.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. &lt;code&gt;SECRET_KEY&lt;/code&gt; Hardcoded or Weakly Generated
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SECRET_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;django-insecure-abc123xyz-changeme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Django's &lt;code&gt;startproject&lt;/code&gt; actually warns you with the &lt;code&gt;django-insecure-&lt;/code&gt; prefix now. But plenty of boilerplates generated before that convention still have static keys checked into version control.&lt;/p&gt;

&lt;p&gt;Even the ones that generate it dynamically sometimes do it wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# generates a NEW key on every restart
&lt;/span&gt;&lt;span class="n"&gt;SECRET_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get_random_secret_key&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means every server restart invalidates all existing sessions and CSRF tokens. That's not a security fix — that's a different bug wearing a security costume.&lt;/p&gt;

&lt;p&gt;The secret key needs to be generated once, stored as an environment variable, and never committed to Git. Full stop.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. &lt;code&gt;ALLOWED_HOSTS = ["*"]&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ALLOWED_HOSTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALLOWED_HOSTS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again — the default is the dangerous value. &lt;code&gt;ALLOWED_HOSTS = ["*"]&lt;/code&gt; disables Django's Host header validation entirely, opening the door for host header injection attacks. This isn't theoretical. Attackers use it to poison password reset emails with their own domain.&lt;/p&gt;

&lt;p&gt;The boilerplate author probably put &lt;code&gt;"*"&lt;/code&gt; there so you can &lt;code&gt;docker-compose up&lt;/code&gt; without configuring anything. Convenience for the developer. Risk for the user.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. CORS Configured Wide Open
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# django-cors-headers
&lt;/span&gt;&lt;span class="n"&gt;CORS_ALLOW_ALL_ORIGINS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CORS exists to protect your users' browsers. When you set this to True, you're telling every origin on the internet that it can make credentialed requests to your API. Combined with &lt;code&gt;SESSION_COOKIE_SAMESITE = None&lt;/code&gt; (which some boilerplates also set), you've recreated the conditions for CSRF attacks in a Single Page App context.&lt;/p&gt;

&lt;p&gt;The reasoning is always the same: &lt;em&gt;"I'll change this when I know my frontend's domain."&lt;/em&gt; But that moment never comes cleanly in the chaos of early development, and it ships.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. No Security Headers
&lt;/h3&gt;

&lt;p&gt;Django doesn't set security headers by default. You need &lt;code&gt;django.middleware.security.SecurityMiddleware&lt;/code&gt; in &lt;code&gt;MIDDLEWARE&lt;/code&gt;, and then you need to actually configure it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SECURE_BROWSER_XSS_FILTER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# deprecated but harmless
&lt;/span&gt;&lt;span class="n"&gt;SECURE_CONTENT_TYPE_NOSNIFF&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;X_FRAME_OPTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DENY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;SECURE_HSTS_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;31536000&lt;/span&gt;
&lt;span class="n"&gt;SECURE_HSTS_INCLUDE_SUBDOMAINS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;SECURE_HSTS_PRELOAD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;SECURE_SSL_REDIRECT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most boilerplates have &lt;code&gt;SecurityMiddleware&lt;/code&gt; in the list. Few have these settings configured. HSTS in particular is almost always missing — which means every user's first request is over HTTP until they get redirected.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Session and Cookie Settings Left at Defaults
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What most boilerplates ship
&lt;/span&gt;&lt;span class="n"&gt;SESSION_COOKIE_HTTPONLY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;   &lt;span class="c1"&gt;# ✅ Django default, usually fine
&lt;/span&gt;&lt;span class="n"&gt;SESSION_COOKIE_SECURE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;    &lt;span class="c1"&gt;# ❌ Default — cookies sent over HTTP
&lt;/span&gt;&lt;span class="n"&gt;CSRF_COOKIE_SECURE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;       &lt;span class="c1"&gt;# ❌ Default — CSRF token sent over HTTP
&lt;/span&gt;&lt;span class="n"&gt;SESSION_COOKIE_SAMESITE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lax&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# ✅ Usually fine, but often overridden
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SESSION_COOKIE_SECURE = True&lt;/code&gt; means the browser will only send the session cookie over HTTPS. Leaving it False means someone on the same network can intercept it over HTTP. This is a solved problem — but it requires explicitly setting it, and boilerplates rarely do.&lt;/p&gt;




&lt;h3&gt;
  
  
  7. Django Admin Sitting on &lt;code&gt;/admin/&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;urlpatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is Django's own default, so I can't blame boilerplate authors entirely. But nobody changes it either. The admin panel at &lt;code&gt;/admin/&lt;/code&gt; is the first thing automated scanners probe. Moving it to something non-guessable costs you nothing and removes you from a significant percentage of automated attacks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-own-path-here/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why This Keeps Happening
&lt;/h2&gt;

&lt;p&gt;The ecosystem isn't malicious. It's optimizing for the wrong metric.&lt;/p&gt;

&lt;p&gt;Boilerplates get stars based on how quickly they get you from zero to running. Security settings that would &lt;em&gt;break&lt;/em&gt; a local setup — like &lt;code&gt;SECURE_SSL_REDIRECT = True&lt;/code&gt; without an SSL cert — get disabled so the DX is smooth. The secure version is the harder version to demo.&lt;/p&gt;

&lt;p&gt;Django itself deserves some of this too. The framework ships with safe defaults in some areas (CSRF protection is on by default, passwords are hashed), but makes you opt-in to the rest. That opt-in surface is where boilerplates consistently fail to follow through.&lt;/p&gt;

&lt;p&gt;And then there's us — developers who clone first, configure later, and ship before "later" arrives.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a Secure Boilerplate Default Looks Like
&lt;/h2&gt;

&lt;p&gt;Split your settings. This is non-negotiable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;settings/
    base.py      # shared across all environments
    local.py     # DEBUG=True, no SSL, relaxed CORS
    production.py  # strict everything
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;production.py&lt;/code&gt;, the defaults should be secure. If something is missing — a &lt;code&gt;SECRET_KEY&lt;/code&gt;, a &lt;code&gt;DATABASE_URL&lt;/code&gt; — the app should refuse to start, not silently fall back to an insecure value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# production.py
&lt;/span&gt;&lt;span class="n"&gt;SECRET_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# KeyError if missing — intentional
&lt;/span&gt;&lt;span class="n"&gt;DEBUG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="n"&gt;ALLOWED_HOSTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALLOWED_HOSTS&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;SECURE_SSL_REDIRECT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;SECURE_HSTS_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;31536000&lt;/span&gt;
&lt;span class="n"&gt;SECURE_HSTS_INCLUDE_SUBDOMAINS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;SESSION_COOKIE_SECURE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;CSRF_COOKIE_SECURE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;CORS_ALLOWED_ORIGINS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CORS_ALLOWED_ORIGINS&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&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;os.environ["KEY"]&lt;/code&gt; without &lt;code&gt;.get()&lt;/code&gt;. If the variable isn't set, it raises a &lt;code&gt;KeyError&lt;/code&gt; and the app doesn't start. That's correct behavior. Failing loudly in production is safer than running silently insecure.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Checklist Before You Ship
&lt;/h2&gt;

&lt;p&gt;If you're using any boilerplate — including one you wrote yourself six months ago — run through this before you deploy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;DEBUG = False&lt;/code&gt; in production, with no unsafe default fallback&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;SECRET_KEY&lt;/code&gt; sourced from environment, never in version control&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt; explicitly set, no wildcards&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;CORS_ALLOW_ALL_ORIGINS = False&lt;/code&gt;, origins explicitly whitelisted&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;SESSION_COOKIE_SECURE = True&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;CSRF_COOKIE_SECURE = True&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;SECURE_SSL_REDIRECT = True&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;SECURE_HSTS_SECONDS&lt;/code&gt; set&lt;/li&gt;
&lt;li&gt;[ ] Django admin URL is not &lt;code&gt;/admin/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;django.middleware.security.SecurityMiddleware&lt;/code&gt; is first in &lt;code&gt;MIDDLEWARE&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run &lt;code&gt;python manage.py check --deploy&lt;/code&gt; too. Django ships a deployment checker that will call out many of these issues. Most developers don't know it exists.&lt;/p&gt;




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

&lt;p&gt;Boilerplates are a starting point, not a finished product. The problem is the word "boilerplate" implies it's something you don't need to think about — just add your code on top. But the security layer is exactly the part that requires thinking.&lt;/p&gt;

&lt;p&gt;The ecosystem needs better defaults. Boilerplate authors should make the &lt;em&gt;safe&lt;/em&gt; behavior the path of least resistance, not the other way around. Django could be more opinionated about production configuration. And we — as developers shipping things to real users — need to stop treating security settings as a post-launch concern.&lt;/p&gt;

&lt;p&gt;It never becomes post-launch. It becomes a breach report.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this useful, I write about Django security, API security, and backend architecture on Dev.to. The next piece will be on OWASP API Top 10 vulnerabilities in real Django REST Framework code — the ones that slip through code review.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Pre-Commit Secret Scanner Because GitHub's Is Too Late</title>
      <dc:creator>siyadhkc</dc:creator>
      <pubDate>Sun, 24 May 2026 15:31:20 +0000</pubDate>
      <link>https://dev.to/siyadhkc/i-built-a-pre-commit-secret-scanner-because-githubs-is-too-late-1eo1</link>
      <guid>https://dev.to/siyadhkc/i-built-a-pre-commit-secret-scanner-because-githubs-is-too-late-1eo1</guid>
      <description>&lt;p&gt;Let me start with a confession.&lt;/p&gt;

&lt;p&gt;I have accidentally committed a &lt;code&gt;.env&lt;/code&gt; file. Not to a private internal repo with no consequences. To a public GitHub repo. With a real Stripe test key in it.&lt;/p&gt;

&lt;p&gt;I caught it within minutes, deleted the file, rotated the key, pushed a new commit. Classic developer scramble. Nothing happened — at least as far as I know.&lt;/p&gt;

&lt;p&gt;But that "as far as I know" lived in my head for a while.&lt;/p&gt;

&lt;p&gt;That experience is what eventually led me to build &lt;strong&gt;env-guard&lt;/strong&gt; — a Python CLI tool that scans your project for secrets and installs as a git pre-commit hook, blocking the commit entirely if it finds something suspicious. The secret never leaves your machine. No scramble. No rotation. No anxiety.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with GitHub Secret Scanning (it's a timing problem)
&lt;/h2&gt;

&lt;p&gt;GitHub has a feature called Secret Scanning. It's legitimately good — it covers dozens of credential formats across major providers and will notify you when it finds something. I'm not here to trash it.&lt;/p&gt;

&lt;p&gt;But it has one fundamental flaw that no amount of good engineering can fix:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It runs after you push.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Think about what that means. By the time GitHub scans your code and sends you an alert, your secret has already:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Left your machine&lt;/li&gt;
&lt;li&gt;Traveled over the internet&lt;/li&gt;
&lt;li&gt;Landed on GitHub's servers&lt;/li&gt;
&lt;li&gt;Been indexed, if the repo is public&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Automated bots scrape public GitHub repos constantly. We're talking seconds after a push, not minutes. GitHub will alert you, but the key is already out there. You're now rotating credentials, auditing API usage logs, and hoping whoever grabbed it hasn't done anything with it yet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Without env-guard:
  code → commit → push → GitHub scans → alert → key already exposed ❌

With env-guard:
  code → commit blocked → fix locally → push clean code ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of GitHub Secret Scanning as your last line of defense. env-guard is your first.&lt;/p&gt;

&lt;p&gt;The secret never enters git history at all. There's nothing to rotate, nothing to audit, and nothing to lose sleep over.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who this is actually for
&lt;/h2&gt;

&lt;p&gt;If you work with any of the following on a regular basis, env-guard is for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API keys&lt;/strong&gt; — OpenAI, Stripe, Twilio, SendGrid, any SaaS you integrate with&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud credentials&lt;/strong&gt; — AWS access keys, GCP service accounts, anything that costs money if misused&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database connection strings&lt;/strong&gt; — postgres://, mongodb://, redis:// — all of these contain passwords&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private keys&lt;/strong&gt; — RSA, EC, OpenSSH — the kind that give access to your servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Basically: if you work on real software that talks to real services, you have secrets somewhere in your local environment. And humans make mistakes. Even careful ones.&lt;/p&gt;

&lt;p&gt;I've seen this happen to senior engineers. It's not a skill issue. It's a missing safety net issue.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it detects
&lt;/h2&gt;

&lt;p&gt;env-guard ships with &lt;strong&gt;54 detection rules&lt;/strong&gt; covering every major category of credential you'd encounter in a typical backend or fullstack project:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;Access Key ID, Secret Access Key, Session Token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;API Key, OAuth Client Secret, Service Account JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub&lt;/td&gt;
&lt;td&gt;Personal Access Token, OAuth Token, App Token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe&lt;/td&gt;
&lt;td&gt;Live and Test Secret Keys, Publishable Keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI / Anthropic&lt;/td&gt;
&lt;td&gt;API Keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack&lt;/td&gt;
&lt;td&gt;Bot Token, User Token, Webhook URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Twilio&lt;/td&gt;
&lt;td&gt;Account SID, Auth Token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SendGrid&lt;/td&gt;
&lt;td&gt;API Key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Databases&lt;/td&gt;
&lt;td&gt;PostgreSQL, MySQL, MongoDB, Redis connection strings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private Keys&lt;/td&gt;
&lt;td&gt;RSA, EC, PGP, OpenSSH private key blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Django&lt;/td&gt;
&lt;td&gt;SECRET_KEY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic&lt;/td&gt;
&lt;td&gt;Password assignments, token assignments, API key assignments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;And more&lt;/td&gt;
&lt;td&gt;Razorpay, NPM, PyPI, Heroku, Netlify, Cloudinary, and others&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each rule carries a severity level — HIGH, MEDIUM, or LOW — so you're not staring at an undifferentiated wall of warnings. A live Stripe secret key is HIGH. A generic-looking token assignment might be MEDIUM. The output is human-readable and actionable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;env-guard scan &lt;span class="nb"&gt;.&lt;/span&gt;

env-guard scan report
Path    : &lt;span class="nb"&gt;.&lt;/span&gt;
Scanned : 42 files
Found   : 1 potential secret&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;

  ✖ HIGH  Stripe Live Secret Key
  File : config/settings.py:12
  Code : STRIPE_KEY &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sk_live_abc123..."&lt;/span&gt;

──────────────────────────────────────────────────
  1 HIGH
──────────────────────────────────────────────────

Scan failed — secrets detected. Do not commit.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;File. Line number. What was found. Severity. No ambiguity, no guesswork — you know exactly where to go and what to fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started in 60 seconds
&lt;/h2&gt;

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

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

&lt;/div&gt;



&lt;p&gt;Requires Python 3.8+. Run a scan on your current project right now:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Go ahead. Run it on a real project you're working on. You might be surprised what's sitting there.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pre-commit hook — the part that actually changes your habits
&lt;/h2&gt;

&lt;p&gt;Ad-hoc scanning is useful for audits, but the real shift happens when env-guard becomes invisible. When you don't have to think about running it. When it just works in the background every single time you commit.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;install-hook&lt;/code&gt; does:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Run that once inside your repo. From that point forward, every &lt;code&gt;git commit&lt;/code&gt; triggers an automatic scan before anything gets written to git history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;env-guard: scanning for secrets...
env-guard: commit blocked. Remove secrets before committing.
env-guard: to skip this check (NOT recommended): git commit --no-verify
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If nothing is found, the commit proceeds normally — you won't even notice it ran. If something is found, the commit is hard-blocked with a clear message telling you exactly where the problem is.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--no-verify&lt;/code&gt; escape hatch exists because I didn't want to be heavy-handed. Sometimes you're committing a test fixture that contains a fake-looking credential. The escape hatch is there — but it requires a conscious decision, and that friction is intentional.&lt;/p&gt;

&lt;p&gt;To remove the hook if you ever need to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;env-guard uninstall-hook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Handling false positives
&lt;/h2&gt;

&lt;p&gt;Every scanner has them. A SHA-256 hash that structurally resembles an API key. A documentation example with a placeholder that matches a pattern. A test fixture with a fake credential for unit tests.&lt;/p&gt;

&lt;p&gt;env-guard handles this with a &lt;code&gt;.envguardignore&lt;/code&gt; file in your project root — same concept as &lt;code&gt;.gitignore&lt;/code&gt;, one pattern per line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ignore specific files
&lt;/span&gt;&lt;span class="err"&gt;tests/fixtures/sample.env&lt;/span&gt;

&lt;span class="c"&gt;# Ignore by extension
&lt;/span&gt;&lt;span class="err"&gt;*.log&lt;/span&gt;

&lt;span class="c"&gt;# Ignore entire directories
&lt;/span&gt;&lt;span class="err"&gt;docs/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it's a committed file, your whole team gets the same exclusions automatically. No per-developer configuration needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  CLI flags for different workflows
&lt;/h2&gt;

&lt;p&gt;A few flags that cover the common situations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Only surface the critical findings&lt;/span&gt;
env-guard scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--severity&lt;/span&gt; HIGH

&lt;span class="c"&gt;# Machine-readable output for scripting&lt;/span&gt;
env-guard scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; json

&lt;span class="c"&gt;# Scan without failing the process (reporting mode)&lt;/span&gt;
env-guard scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--no-fail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--no-fail&lt;/code&gt; flag is worth calling out specifically. If you're adding env-guard to an existing large codebase, you probably don't want to immediately block everything and deal with 50 findings on day one. Run with &lt;code&gt;--no-fail&lt;/code&gt; first, triage what's real vs. false positive, set up your &lt;code&gt;.envguardignore&lt;/code&gt;, then remove the flag when you're confident. Gradual adoption is a completely valid path.&lt;/p&gt;




&lt;h2&gt;
  
  
  For teams: CI/CD as a second checkpoint
&lt;/h2&gt;

&lt;p&gt;Pre-commit hooks protect your own machine. But what about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;External contributors who clone the repo and don't run &lt;code&gt;install-hook&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;New team members who haven't set up their environment yet&lt;/li&gt;
&lt;li&gt;Automated processes that generate config files with credentials&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For that, env-guard works in CI pipelines too. Here's a GitHub Actions workflow you can drop into any repo:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret Scan&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install env-guard&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;env-guard scan .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you the same local protection applied to every PR, from every contributor, on every push. env-guard locally + this workflow in CI = defense in depth that actually makes sense.&lt;/p&gt;




&lt;h2&gt;
  
  
  env-guard vs GitHub Secret Scanning — use both
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;env-guard&lt;/th&gt;
&lt;th&gt;GitHub Secret Scanning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;When it runs&lt;/td&gt;
&lt;td&gt;Before commit, on your machine&lt;/td&gt;
&lt;td&gt;After push, on GitHub's servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blocks the secret&lt;/td&gt;
&lt;td&gt;Yes — commit is blocked&lt;/td&gt;
&lt;td&gt;No — secret is already pushed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works offline&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom ignore rules&lt;/td&gt;
&lt;td&gt;Yes, &lt;code&gt;.envguardignore&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (public repos)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requires GitHub&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These aren't competing tools. They protect different parts of the pipeline. Run env-guard locally to stop secrets before they ever leave your machine. Keep GitHub Secret Scanning on as a fallback for anything that slips through — external contributors, edge cases, human error. Use both.&lt;/p&gt;




&lt;h2&gt;
  
  
  The thing I keep thinking about
&lt;/h2&gt;

&lt;p&gt;There's a concept in security called "shift left" — the idea that you should catch problems as early in the development process as possible, because the earlier you catch something, the cheaper it is to fix and the less damage it causes.&lt;/p&gt;

&lt;p&gt;A leaked credential caught before the commit? Zero cost. Rotate nothing. Tell no one. Move on.&lt;/p&gt;

&lt;p&gt;A leaked credential caught after a push to a public repo? Rotate the key, audit usage logs, send an incident report if it's a team environment, and spend a few days with that low-grade anxiety of "did anything actually get accessed?"&lt;/p&gt;

&lt;p&gt;env-guard is a shift-left tool. The entire design philosophy is: don't give the secret a chance to travel. Keep it local. Catch it at the source.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&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;env-guard
env-guard scan &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want the hook:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Source on GitHub: &lt;a href="https://github.com/siyadhkc/env-guard" rel="noopener noreferrer"&gt;github.com/siyadhkc/env-guard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If it catches something on the first scan — good. That's the point. And if you hit a false positive that's annoying, open an issue. I want the pattern library to be genuinely useful, not just technically comprehensive.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's coming next
&lt;/h2&gt;

&lt;p&gt;A few things on the roadmap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-push hook option&lt;/strong&gt; — a second checkpoint right before the push, for teams who want both&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More detection rules&lt;/strong&gt; — 54 is solid but the ecosystem keeps growing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project-level config file&lt;/strong&gt; — custom severity thresholds, rule toggles, team-specific settings beyond just ignores&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this is useful to you, star the repo and watch for updates. And if you've ever had a bad day because of an exposed credential — you're not alone, and this is for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Python. MIT licensed. Contributions welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>python</category>
      <category>devto</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built a multi-tenant food delivery platform alone. Here's what nobody tells you about that.</title>
      <dc:creator>siyadhkc</dc:creator>
      <pubDate>Sun, 17 May 2026 16:40:10 +0000</pubDate>
      <link>https://dev.to/siyadhkc/i-built-a-multi-tenant-food-delivery-platform-alone-heres-what-nobody-tells-you-about-that-104c</link>
      <guid>https://dev.to/siyadhkc/i-built-a-multi-tenant-food-delivery-platform-alone-heres-what-nobody-tells-you-about-that-104c</guid>
      <description>&lt;p&gt;Four user roles. One payment gateway that lied to me. Three rewrites of the same feature. And a GPS bug that nearly flooded my database. This is the real story behind Savor.&lt;/p&gt;




&lt;h2&gt;
  
  
  I was tired of building things that felt fake
&lt;/h2&gt;

&lt;p&gt;At some point every developer hits a wall with tutorial projects. You've built the weather app. You've done the blog CRUD. You've cloned Twitter in a weekend. And none of it actually &lt;em&gt;teaches&lt;/em&gt; you what it feels like when four different types of users are all touching the same data at the same time, and you have to keep their worlds completely separate.&lt;/p&gt;

&lt;p&gt;I wanted something that would break me a little. Something with real business logic, real money, real state synchronization problems. Food delivery checked every single box.&lt;/p&gt;

&lt;p&gt;Customers order food. Restaurants fulfill it. Delivery agents pick it up. Admins oversee everything. Four completely different actors, all interacting with overlapping data, all in real time. That's not a tutorial problem — that's a systems problem. I called it &lt;strong&gt;Savor&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Building this was the first time I genuinely didn't know if something would work until I ran it. That was terrifying and that was the point."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  These aren't exciting choices. They're correct ones.
&lt;/h2&gt;

&lt;p&gt;Django REST Framework on the backend. React 19 + Vite on the frontend. PostgreSQL. I didn't pick these to be cool. I picked them because the problem needed a mature ORM, ACID-compliant transactions, and a frontend that wouldn't fight me on state management.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;th&gt;Why it mattered&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Django 5.1 + DRF&lt;/td&gt;
&lt;td&gt;Atomic transactions, battle-tested ORM, signals for cross-model side effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;React 19 + Vite&lt;/td&gt;
&lt;td&gt;Fast HMR, modern concurrent features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;SimpleJWT + rotation&lt;/td&gt;
&lt;td&gt;Silent 60-min access token rotation, 7-day refresh — no mid-session logouts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;ACID compliance. Nested financial data cannot partially save.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Razorpay&lt;/td&gt;
&lt;td&gt;Localized processing, webhook verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Media&lt;/td&gt;
&lt;td&gt;Cloudinary CDN&lt;/td&gt;
&lt;td&gt;Edge delivery. Serving images from Django would be career-ending performance-wise.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;Tailwind CSS 4.0&lt;/td&gt;
&lt;td&gt;Zero runtime. Design tokens kept four portals visually coherent.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animations&lt;/td&gt;
&lt;td&gt;Framer Motion&lt;/td&gt;
&lt;td&gt;Micro-interactions. A premium feel isn't just design — it's movement.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mapping&lt;/td&gt;
&lt;td&gt;Leaflet.js&lt;/td&gt;
&lt;td&gt;Live GPS plotting on the customer tracking view&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one decision I'd revisit: React Context API over Zustand. Context was fine for the scope but I started feeling prop drilling pain by week four. If you're building something this complex, just start with Zustand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Each portal was its own distinct problem
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The customer app&lt;/strong&gt; was where I started because it's the most tangible — you can see it, click it, feel it. The discovery architecture has two layers: global cuisines (Arabian, South Indian, Desserts) and restaurant-internal categories (Must Try, Specials). Getting that hierarchy right in the serializers took four iterations. The cart was where I first hit real business logic — you can't add items from two different restaurants to the same cart. I built persistent local state that checks restaurant ID on every cart mutation. Sounds simple. It wasn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The restaurant partner portal&lt;/strong&gt; was the one I underestimated the most. I assumed it'd be a dashboard with some CRUD. What I got was the most complex portal in the system. Restaurant owners are power users. The order lifecycle — Pending → Accept → Preparing → Ready for Delivery — cascades into downstream state changes across three other portals simultaneously. I used Django signals for this. Required very careful thinking about idempotency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The delivery agent interface&lt;/strong&gt; is the one I'm most proud of technically. Geo-fencing logic is implemented at the queryset manager level, not the view level. A Kochi agent cannot see a Mumbai dispatch — and this isn't a frontend filter someone can bypass with a crafted request. The data simply doesn't exist in the response. Getting the earnings calculation to be atomic and accurate on the &lt;code&gt;Delivered&lt;/code&gt; status transition took three rewrites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The admin console&lt;/strong&gt; could have been an afterthought. I didn't let it be. With 3,000+ menu items seeded from the Kerala restaurant dataset, server-side pagination was non-negotiable. I implemented cursor-based pagination — offset pagination on PostgreSQL becomes unusably slow past 1,000 rows.&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%2Faci92qhdruwgwaytbbid.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%2Faci92qhdruwgwaytbbid.png" alt="website" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
Live demo: &lt;a href="https://food-delivery-seven-bice.vercel.app" rel="noopener noreferrer"&gt;food-delivery-seven-bice.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The struggles nobody writes about in project writeups
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;01 — Multi-tenancy took three complete rewrites&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first version filtered at the serializer level. That works until a restaurant owner in tenant A briefly sees an order from tenant B during a race condition. The second version moved filters to the view layer — better, still bypassable. The third version put tenant scoping into the queryset manager. That's where it needed to be from the start. Two full days of debugging a bug that shouldn't have existed if I'd thought it through properly the first time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;02 — Razorpay webhook verification in development is a trap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Webhooks need a publicly accessible endpoint. In development, you don't have one. I spent a full day debugging a signature verification failure that turned out to be a timezone mismatch in how I was constructing the HMAC payload. I ended up building a local webhook simulator specifically for dev testing, stripped out before production. This is not in any Razorpay tutorial I found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;03 — GPS telemetry nearly destroyed my database&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Early testing: delivery agents broadcast coordinates on every geolocation API event. The browser's &lt;code&gt;watchPosition&lt;/code&gt; fires multiple times per second. At 10 concurrent test agents, that was hundreds of database writes per minute for a field that only needs to update every few seconds. I added a frontend debounce and a backend timestamp guard that rejects updates more frequent than 3 seconds. The database survived. My nerves less so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;04 — CSS across four portals with four different visual languages&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The customer app is warm and premium. The delivery interface is utilitarian and fast. The admin console is dense and data-heavy. The partner portal is somewhere between all three. Tailwind's design token system helped, but by week three the tokens were drifting in ways that required a full audit to fix. CSS architecture is genuinely hard at scale and nobody talks about the multi-portal version of this problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;05 — The cart constraint bug that appeared in production and nowhere else&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cart state was using localStorage with a restaurant ID key. In production, the Vercel edge occasionally delivered a cached app version mid-session with a stale cart key. The constraint check passed because the key comparison was against an old restaurant ID. Never reproduced locally. Fixed by adding a session-scoped cart version hash alongside the restaurant ID. The worst kind of bug: requires production traffic to surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one decision that saved the whole system
&lt;/h2&gt;

&lt;p&gt;Early on I had a choice: enforce multi-tenancy at the UI layer or the data layer. The UI layer is easier — you just filter what you show. The data layer is harder — you modify how querysets are constructed at the model level so filtered-out data is never even fetched.&lt;/p&gt;

&lt;p&gt;I chose the data layer. Every restaurant-scoped query goes through a custom queryset manager that injects the tenant ID before the SQL hits PostgreSQL. Bypassing tenant isolation requires compromising the Django application layer itself — not just crafting a request with different parameters.&lt;/p&gt;

&lt;p&gt;For financial data, I used &lt;code&gt;@transaction.atomic&lt;/code&gt; everywhere that touched nested profile updates. A restaurant's payout configuration and platform commission rate update in a single atomic operation. Either both succeed or neither does. This is not optional when you're dealing with money.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The architecture that survives isn't the one that looks cleanest in a diagram. It's the one that fails gracefully when things go wrong — and they will go wrong."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  You only understand failure modes by creating them
&lt;/h2&gt;

&lt;p&gt;You can read about multi-tenancy. You can read about JWT rotation. You can understand atomic transactions in the abstract. But there's a kind of understanding you only get from tracing a data leakage bug through four layers of abstraction at 11pm, finding the one queryset that forgot to filter by &lt;code&gt;restaurant_id&lt;/code&gt;, and sitting with the fact that your first two architectural decisions were wrong.&lt;/p&gt;

&lt;p&gt;Building Savor taught me that system design isn't about choosing the right technologies. It's about understanding the failure modes of every interface between them. The Razorpay integration fails if my webhook handler is slow. GPS tracking floods my database if the frontend isn't debounced. Multi-tenant isolation breaks if I filter at the wrong layer. None of these are things you learn by reading. You learn them by breaking them, badly, and having to fix it.&lt;/p&gt;

&lt;p&gt;If you're in the middle of a solo project right now and it feels like the complexity is winning — it probably is, temporarily. That's what week three feels like. The tangle is the work. Push through it.&lt;/p&gt;




&lt;p&gt;Live demo: &lt;a href="https://food-delivery-seven-bice.vercel.app" rel="noopener noreferrer"&gt;food-delivery-seven-bice.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Backend API: &lt;a href="https://food-delivery-backend-c4pe.onrender.com/api" rel="noopener noreferrer"&gt;food-delivery-backend-c4pe.onrender.com/api&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Github: &lt;a href="https://github.com/siyadhkc/savor" rel="noopener noreferrer"&gt;food-delivery-github&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Postman collection at &lt;code&gt;backend/postman.json&lt;/code&gt; — all auth rotation, order status, and multi-tenant profile endpoints pre-configured.&lt;/p&gt;




</description>
      <category>django</category>
      <category>react</category>
      <category>fullstack</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a JSON CLI tool with Bun and TypeScript — here's what actually happened</title>
      <dc:creator>siyadhkc</dc:creator>
      <pubDate>Mon, 11 May 2026 01:31:57 +0000</pubDate>
      <link>https://dev.to/siyadhkc/i-built-a-json-cli-tool-with-bun-and-typescript-heres-what-actually-happened-28ek</link>
      <guid>https://dev.to/siyadhkc/i-built-a-json-cli-tool-with-bun-and-typescript-heres-what-actually-happened-28ek</guid>
      <description>&lt;p&gt;I wanted a tool that could flatten JSON, filter it, colorize output, and fetch from a URL — all from&lt;br&gt;
the terminal. Something between &lt;code&gt;jq&lt;/code&gt; and &lt;code&gt;gron&lt;/code&gt; but with a cleaner, more intuitive API. So I built&lt;br&gt;
&lt;code&gt;jray&lt;/code&gt;. This is the honest post-mortem of how it went.&lt;/p&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/siyadhkc/jray" rel="noopener noreferrer"&gt;github.com/siyadhkc/jray&lt;/a&gt;&lt;br&gt;
Website:&lt;a href="https://siyadhkc.github.io/Jray" rel="noopener noreferrer"&gt;siyadhkc.github.io/Jray&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;Every time I'm debugging an API response or poking around some nested config file, I reach for &lt;code&gt;jq&lt;/code&gt;.&lt;br&gt;
And every time, I spend the first five minutes googling the syntax again. &lt;code&gt;jq&lt;/code&gt; is powerful — genuinely&lt;br&gt;
— but it's its own language, and unless you're writing complex transformations daily, that overhead&lt;br&gt;
adds up.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gron&lt;/code&gt; is the other tool I kept coming back to. It flattens JSON into greppable dot-notation, which&lt;br&gt;
is exactly what you want when you're trying to find where a deeply nested key lives. But &lt;code&gt;gron&lt;/code&gt; is&lt;br&gt;
read-only. You can't filter and get structured output back. You can't fetch from a URL. The color&lt;br&gt;
situation is nonexistent.&lt;/p&gt;

&lt;p&gt;So I scoped out what I actually needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flatten JSON to dot-notation paths (&lt;code&gt;a.b.c = value&lt;/code&gt;) so I can see structure at a glance&lt;/li&gt;
&lt;li&gt;Filter by key pattern without learning a query language&lt;/li&gt;
&lt;li&gt;Fetch directly from a URL so I don't have to &lt;code&gt;curl | jray&lt;/code&gt; every time&lt;/li&gt;
&lt;li&gt;Color output by default on TTY&lt;/li&gt;
&lt;li&gt;Fast enough that it doesn't feel like overhead when I'm in the middle of something&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bun was the obvious runtime choice. Fast cold start, native TypeScript support, a built-in test&lt;br&gt;
runner, and &lt;code&gt;bun build --compile&lt;/code&gt; that bundles everything into a single self-contained binary. No&lt;br&gt;
Node.js version headaches, no separate install step for the runtime.&lt;/p&gt;


&lt;h2&gt;
  
  
  How it's structured
&lt;/h2&gt;

&lt;p&gt;I kept the architecture intentionally flat. No frameworks, minimal dependencies. The core logic is&lt;br&gt;
split into focused modules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;flatten.ts&lt;/code&gt; — recursive flatten and unflatten. Takes a nested object and produces a map of
dot-notation paths to primitive values. Unflatten goes the other direction.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;filter.ts&lt;/code&gt; — glob-style pattern matching on the flattened keys. &lt;code&gt;user.*&lt;/code&gt; gives you everything
under &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;**.id&lt;/code&gt; matches any &lt;code&gt;id&lt;/code&gt; at any depth.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;color.ts&lt;/code&gt; — ANSI color output. Keys in one color, values type-aware (strings, numbers, booleans
each get their own).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fetch.ts&lt;/code&gt; — thin wrapper around &lt;code&gt;Bun.fetch&lt;/code&gt; that handles the request, checks content type, and
pipes the response into the flatten pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cli.ts&lt;/code&gt; — argument parsing and wiring. Reads from stdin, a file path, or a URL depending on what
flags you pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing compiles down to a single binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun build ./src/cli.ts &lt;span class="nt"&gt;--compile&lt;/span&gt; &lt;span class="nt"&gt;--outfile&lt;/span&gt; jray
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No runtime dependency on Bun after that. You ship the binary, it works.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problems I actually ran into
&lt;/h2&gt;

&lt;h3&gt;
  
  
  npm name conflict
&lt;/h3&gt;

&lt;p&gt;First thing I did after writing the initial working version was try to publish as &lt;code&gt;jray&lt;/code&gt;. Already&lt;br&gt;
taken — and not squatted. An actual published package with actual users. So I had to switch to a&lt;br&gt;
scoped package: &lt;code&gt;@siyadkc/jray&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That sounds like a small change but it cascades. The README, the install command in all the&lt;br&gt;
examples, the binary name in &lt;code&gt;package.json&lt;/code&gt;, the GitHub Actions publish step, the Dev.to article&lt;br&gt;
draft I'd already half-written — all of it needed updating. It also changes how users install it&lt;br&gt;
slightly, since scoped packages require the &lt;code&gt;@&lt;/code&gt; prefix. Not a dealbreaker, but check npm before you&lt;br&gt;
name your CLI.&lt;/p&gt;
&lt;h3&gt;
  
  
  Package size bloat
&lt;/h3&gt;

&lt;p&gt;My first publish was heavier than it needed to be. Bun's compiled binary includes the runtime,&lt;br&gt;
which is expected and fine. But the npm package itself had no &lt;code&gt;files&lt;/code&gt; field, so it was packaging&lt;br&gt;
everything — source maps, test files, the &lt;code&gt;node_modules&lt;/code&gt; from dev, local configs. None of that&lt;br&gt;
belongs in a published package.&lt;/p&gt;

&lt;p&gt;Fix was straightforward:&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="nl"&gt;"files"&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="s2"&gt;"dist/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"README.md"&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;That single field cut the published package weight significantly. Always define &lt;code&gt;files&lt;/code&gt;. The npm&lt;br&gt;
registry doesn't know what's dev and what's production unless you tell it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Windows compatibility
&lt;/h3&gt;

&lt;p&gt;I had a &lt;code&gt;postinstall&lt;/code&gt; script that ran &lt;code&gt;chmod +x&lt;/code&gt; on the binary. Works perfectly on Linux and macOS.&lt;br&gt;
On Windows it throws and the install fails. I wrapped it in a try-catch and updated the docs to note&lt;br&gt;
that Windows users should either run the binary path directly or use WSL. Not the cleanest solution,&lt;br&gt;
but honest about the platform situation. Full Windows support is on the roadmap.&lt;/p&gt;
&lt;h3&gt;
  
  
  GitHub Actions for CI and publishing
&lt;/h3&gt;

&lt;p&gt;The pipeline itself wasn't complicated — install Bun, run tests, publish to npm on a version tag.&lt;br&gt;
But there's one specific thing that tripped me up: not using &lt;code&gt;--frozen-lockfile&lt;/code&gt; in CI.&lt;/p&gt;

&lt;p&gt;Without it, &lt;code&gt;bun install&lt;/code&gt; in CI can silently update the lockfile, which means your CI might be&lt;br&gt;
testing against different dependency versions than what you developed against. Always use:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oven-sh/setup-bun@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;bun-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bun install --frozen-lockfile&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bun test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publishing to npm from Actions requires setting &lt;code&gt;NPM_TOKEN&lt;/code&gt; as a repository secret and passing it&lt;br&gt;
through to the publish step. Once that's wired, releases are a tag push away.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install&lt;/span&gt;
bun add &lt;span class="nt"&gt;-g&lt;/span&gt; @siyadkc/jray

&lt;span class="c"&gt;# Flatten JSON from stdin&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"user":{"name":"Eliot","age":24}}'&lt;/span&gt; | jray
&lt;span class="c"&gt;# user.name = "Eliot"&lt;/span&gt;
&lt;span class="c"&gt;# user.age = 24&lt;/span&gt;

&lt;span class="c"&gt;# Flatten a local file&lt;/span&gt;
jray data.json

&lt;span class="c"&gt;# Filter by key pattern&lt;/span&gt;
jray &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s2"&gt;"user.*"&lt;/span&gt; data.json
&lt;span class="c"&gt;# user.name = "Eliot"&lt;/span&gt;
&lt;span class="c"&gt;# user.age = 24&lt;/span&gt;

&lt;span class="c"&gt;# Fetch from a URL and flatten&lt;/span&gt;
jray &lt;span class="nt"&gt;--fetch&lt;/span&gt; https://api.example.com/users/1

&lt;span class="c"&gt;# Disable color (for piping into other tools)&lt;/span&gt;
jray &lt;span class="nt"&gt;--no-color&lt;/span&gt; data.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filter uses glob-style matching against the full dot-path of each key. So &lt;code&gt;user.*&lt;/code&gt; matches one&lt;br&gt;
level under &lt;code&gt;user&lt;/code&gt;, while &lt;code&gt;**.id&lt;/code&gt; would match any &lt;code&gt;id&lt;/code&gt; at any depth in the document. It's&lt;br&gt;
intentionally simpler than jq's filter syntax — the goal is something you can remember without&lt;br&gt;
looking it up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it sits vs the competition
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gron&lt;/code&gt; is the closest thing and the most direct comparison. It also flattens to dot-notation and&lt;br&gt;
it's what made me realize there was a gap to fill. But &lt;code&gt;gron&lt;/code&gt; has no color output, no URL fetching,&lt;br&gt;
and no NDJSON streaming support. &lt;code&gt;jray&lt;/code&gt; has all three.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;jq&lt;/code&gt; is in a different category. It's a transformation tool — you write expressions, you reshape&lt;br&gt;
data, you do complex operations. &lt;code&gt;jray&lt;/code&gt; is an inspection tool. You throw data at it and you&lt;br&gt;
understand the structure immediately. They're complementary more than they're competing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fx&lt;/code&gt; is another one that comes up. Interactive, powerful, but it's a TUI. If you're in a script or&lt;br&gt;
piping between tools, you don't want a TUI. &lt;code&gt;jray&lt;/code&gt; is designed to be pipeline-friendly from the&lt;br&gt;
start.&lt;/p&gt;

&lt;p&gt;The longer-term vision is broader than just JSON. Supporting YAML and TOML with the same flatten&lt;br&gt;
and filter interface makes it a universal structured-data CLI. There's also TOON (Token Oriented&lt;br&gt;
Object Notation), which is an emerging format that's genuinely useful for LLM developer workflows&lt;br&gt;
since it's optimized for tokenization. That's a real differentiator if the format gets traction.&lt;/p&gt;




&lt;h2&gt;
  
  
  The launch
&lt;/h2&gt;

&lt;p&gt;Published to npm, wrote this article, posted on X . Also shared in the Bun&lt;br&gt;
Discord since the community there is small enough that people actually read it.&lt;/p&gt;

&lt;p&gt;The GitHub Pages site is live. It's getting redesigned right now — moving toward a flatter, more&lt;br&gt;
minimal aesthetic with an interactive playground section where you can paste JSON directly in the&lt;br&gt;
browser and see the flatten and filter output in real time. No install required to try it. That&lt;br&gt;
single feature will probably do more for adoption than any amount of posting.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;I'd define the &lt;code&gt;files&lt;/code&gt; field before the first publish, not after. I'd also check npm for name&lt;br&gt;
conflicts before I was emotionally attached to the name. And I'd set up the CI pipeline before&lt;br&gt;
the first push to &lt;code&gt;main&lt;/code&gt; so I wasn't debugging GitHub Actions at the same time I was trying to&lt;br&gt;
ship.&lt;/p&gt;

&lt;p&gt;The architecture decisions held up fine. Bun was the right call. The modular structure meant I&lt;br&gt;
could add &lt;code&gt;fetch.ts&lt;/code&gt; and &lt;code&gt;color.ts&lt;/code&gt; without touching the core flatten logic. That's about as much&lt;br&gt;
as you can ask from a first project's structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add &lt;span class="nt"&gt;-g&lt;/span&gt; @siyadkc/jray
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/siyadhkc/jray" rel="noopener noreferrer"&gt;github.com/siyadhkc/jray&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're working on something similar with Bun, or you have opinions on the filter API design,&lt;br&gt;
drop a comment. Always curious how other people are solving the same problems.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is my first article writing on this platform so if i made any narration mistake or anything feel free to tell me, i will improve my writing skill and the way of telling this&lt;br&gt;
-- let's get connected --&lt;/em&gt; &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>opensource</category>
      <category>cli</category>
    </item>
    <item>
      <title>From Hardware to Hacking: How Understanding Systems Changed My Approach to Security</title>
      <dc:creator>siyadhkc</dc:creator>
      <pubDate>Wed, 15 Apr 2026 15:56:13 +0000</pubDate>
      <link>https://dev.to/siyadhkc/from-hardware-to-hacking-how-understanding-systems-changed-my-approach-to-security-4dh4</link>
      <guid>https://dev.to/siyadhkc/from-hardware-to-hacking-how-understanding-systems-changed-my-approach-to-security-4dh4</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I didn’t start my journey in cybersecurity. I started with hardware and networking, learning how systems actually function at a low level — how machines communicate, how configurations impact behavior, and how failures happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shift to Development
&lt;/h2&gt;

&lt;p&gt;Later, I moved into development, working with Python and full-stack technologies. Building applications helped me understand how software is structured, but it also revealed something important:&lt;/p&gt;

&lt;p&gt;Most applications are not built with security as a priority.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Turning Point
&lt;/h2&gt;

&lt;p&gt;This realization pushed me toward cybersecurity, specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web application security&lt;/li&gt;
&lt;li&gt;API security&lt;/li&gt;
&lt;li&gt;Cloud platforms (AWS, Azure, GCP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of just building systems, I began analyzing how they can be exploited.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Different Way of Thinking
&lt;/h2&gt;

&lt;p&gt;The biggest shift was in mindset:&lt;/p&gt;

&lt;p&gt;From:&lt;br&gt;
“How do I build this system?”&lt;/p&gt;

&lt;p&gt;To:&lt;br&gt;
“How can this system fail or be attacked?”&lt;/p&gt;

&lt;p&gt;This dual perspective — builder and attacker — is critical in modern cybersecurity.&lt;/p&gt;

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

&lt;p&gt;I’ll be sharing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Practical security insights&lt;/li&gt;
&lt;li&gt;Real-world learning experiences&lt;/li&gt;
&lt;li&gt;Breakdowns of vulnerabilities and misconfigurations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re interested in understanding systems deeply — not just building them, but also testing their limits — follow along.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>tutorial</category>
      <category>career</category>
    </item>
    <item>
      <title>I built a modern alternative to gron using Bun and TypeScript</title>
      <dc:creator>siyadhkc</dc:creator>
      <pubDate>Sun, 12 Apr 2026 17:16:36 +0000</pubDate>
      <link>https://dev.to/siyadhkc/i-built-a-modern-alternative-to-gron-using-bun-and-typescript-3358</link>
      <guid>https://dev.to/siyadhkc/i-built-a-modern-alternative-to-gron-using-bun-and-typescript-3358</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Working with JSON in the terminal is painful. &lt;code&gt;jq&lt;/code&gt; is powerful &lt;br&gt;
but requires learning a whole query language. &lt;code&gt;grep&lt;/code&gt; works but &lt;br&gt;
matches values too — not just paths.&lt;/p&gt;

&lt;p&gt;For example if I search for "name" in my JSON:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grep matches &lt;code&gt;json.user.name&lt;/code&gt; ✅ (correct)&lt;/li&gt;
&lt;li&gt;grep also matches &lt;code&gt;json.bio = "my name is Alice"&lt;/code&gt; ❌ (wrong)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;strong&gt;jray&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What jray does
&lt;/h2&gt;

&lt;p&gt;jray flattens any JSON into one line per value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;jray data.json
json.organization.name &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Acme Corporation"&lt;/span&gt;
json.users[0].name &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Alice Pemberton"&lt;/span&gt;
json.users[0].role &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"admin"&lt;/span&gt;
json.users[0].active &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;json.billing.plan &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"enterprise"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can grep it, awk it, pipe it — using tools you already know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four commands that cover everything
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Flatten with color output&lt;/span&gt;
jray data.json

&lt;span class="c"&gt;# Fetch a URL directly — no curl needed&lt;/span&gt;
jray https://jsonplaceholder.typicode.com/users/1

&lt;span class="c"&gt;# Filter by path only (never matches values)&lt;/span&gt;
jray data.json &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s2"&gt;"billing"&lt;/span&gt;

&lt;span class="c"&gt;# Extract any subtree as clean JSON&lt;/span&gt;
jray data.json &lt;span class="nt"&gt;--select&lt;/span&gt; &lt;span class="s2"&gt;"billing"&lt;/span&gt;

&lt;span class="c"&gt;# Print just the values&lt;/span&gt;
jray data.json &lt;span class="nt"&gt;--values&lt;/span&gt;

&lt;span class="c"&gt;# Reconstruct original JSON&lt;/span&gt;
jray data.json | jray &lt;span class="nt"&gt;--ungron&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The comparison with existing tools
&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;jray&lt;/th&gt;
&lt;th&gt;jq&lt;/th&gt;
&lt;th&gt;gron&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flatten to lines&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconstruct JSON&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter by path&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract subtree&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fetch from URLs&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Color output&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No query language&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript / modern&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I learned building it
&lt;/h2&gt;

&lt;p&gt;This was my first open source CLI tool. Key lessons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Separate your CLI from your logic&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;cli.ts&lt;/code&gt; handles only I/O — reading files, printing output.&lt;br&gt;
&lt;code&gt;flatten.ts&lt;/code&gt;, &lt;code&gt;filter.ts&lt;/code&gt; etc. know nothing about terminals.&lt;br&gt;
This makes everything testable in isolation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Use &lt;code&gt;unknown&lt;/code&gt; not &lt;code&gt;any&lt;/code&gt; in TypeScript&lt;/strong&gt;&lt;br&gt;
JSON is dynamic but that doesn't mean &lt;code&gt;any&lt;/code&gt;. Using &lt;code&gt;unknown&lt;/code&gt; &lt;br&gt;
forces you to check types before using values — much safer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Recursive functions are perfect for JSON&lt;/strong&gt;&lt;br&gt;
JSON is a tree. Walking a tree with recursion is the &lt;br&gt;
cleanest pattern — each call handles one node and &lt;br&gt;
delegates children back to itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Bun is genuinely fast and simple&lt;/strong&gt;&lt;br&gt;
Native TypeScript, built-in fetch, fast startup. &lt;br&gt;
For a CLI tool, Bun is a great choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The README matters as much as the code&lt;/strong&gt;&lt;br&gt;
Nobody uses a tool they can't understand in 30 seconds.&lt;br&gt;
Writing clear examples with real output is as important &lt;br&gt;
as writing the code itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  Install and try it
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @siyadkc/jray
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jray https://jsonplaceholder.typicode.com/users/1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/siyadhkc/Jray" rel="noopener noreferrer"&gt;github.com/siyadhkc/Jray&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;for more info Website: &lt;a href="https://siyadhkc.github.io/Jray/" rel="noopener noreferrer"&gt;https://siyadhkc.github.io/Jray/&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;This is my first open source tool. Feedback on the code &lt;br&gt;
quality is very welcome — especially from experienced &lt;br&gt;
TypeScript developers!&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>bunjs</category>
      <category>opensource</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
