<?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: Toprak</title>
    <description>The latest articles on DEV Community by Toprak (@yagcioglutoprak).</description>
    <link>https://dev.to/yagcioglutoprak</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%2F3794323%2Fe80e5e50-0a2d-4eed-abf2-ae1fafcfe6c1.png</url>
      <title>DEV Community: Toprak</title>
      <link>https://dev.to/yagcioglutoprak</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yagcioglutoprak"/>
    <language>en</language>
    <item>
      <title>How I made it impossible for my Mac cleaner to delete the wrong thing</title>
      <dc:creator>Toprak</dc:creator>
      <pubDate>Wed, 03 Jun 2026 18:21:14 +0000</pubDate>
      <link>https://dev.to/yagcioglutoprak/how-i-made-it-impossible-for-my-mac-cleaner-to-delete-the-wrong-thing-5ahb</link>
      <guid>https://dev.to/yagcioglutoprak/how-i-made-it-impossible-for-my-mac-cleaner-to-delete-the-wrong-thing-5ahb</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally written for &lt;a href="https://toprak.sh/dusty" rel="noopener noreferrer"&gt;Dusty&lt;/a&gt;, a free, open-source macOS disk cleaner.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I kept running out of disk on a 512 GB MacBook, and I could never tell where the space went. It was always caches I had forgotten about: Xcode DerivedData, a pile of old simulators, npm and pip and gradle leftovers. The annoying part was never the cleanup. It was that every tool I tried wanted me to trust it. You press one button, a progress bar says it freed 14 GB, and you have no idea what those 14 GB were. That works right up until the day it deletes something you needed.&lt;/p&gt;

&lt;p&gt;So I wrote my own, and the whole thing started from one rule: it should be physically unable to delete the wrong thing, even if I write a bug. This is how that rule turned into code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Allowlist, not denylist
&lt;/h2&gt;

&lt;p&gt;The usual way to make a cleaner "safe" is a blocklist: delete anything except the things on a list of protected folders. That is backwards. A blocklist is safe until you forget an entry, and you will forget an entry. The failure mode is "oops, that one wasn't on the list."&lt;/p&gt;

&lt;p&gt;An allowlist fails the other way. It refuses to delete something it could safely have deleted. That is the side I want to fail on. So a path is deletable only if it sits inside a known, named cache directory that someone deliberately added. Everything else is rejected by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  One door for every delete
&lt;/h2&gt;

&lt;p&gt;All of the deletion logic lives in a separate Swift package with its own unit tests, not buried in the UI. Inside it there is exactly one function that every candidate path has to pass through before anything is removed: &lt;code&gt;validateDeletionPath&lt;/code&gt;. The UI cannot delete a file. It can only ask the validator, which returns a typed &lt;code&gt;Result&lt;/code&gt; that is either &lt;code&gt;.success&lt;/code&gt; or a specific &lt;code&gt;SafetyError&lt;/code&gt;. There is no path around it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;validateDeletionPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CleanupTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;allowlistedRoots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;SafetyError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;allowedTargetIDs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pathNotInAllowlist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;containsPathTraversal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                 &lt;span class="c1"&gt;// reject anything with ".."&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pathTraversal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;standardized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;NSString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standardizingPath&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;fileURLWithPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;standardized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;isDirectory&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="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isSymlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                           &lt;span class="c1"&gt;// never follow a symlinked leaf&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;symlinkRefusal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;standardized&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="nf"&gt;matchesProhibitedPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;standardized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;          &lt;span class="c1"&gt;// Documents, Photos, Mail, Keychains...&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prohibitedPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;standardized&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="nf"&gt;isPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;standardized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;underAnyOf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;allowlistedRoots&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pathNotInAllowlist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;standardized&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="nf"&gt;isPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;standardized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;underAnyOfResolvingSymlinks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;allowlistedRoots&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;symlinkRefusal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;standardized&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;// ancestor symlink defense&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isOnBootVolume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                          &lt;span class="c1"&gt;// no external drives, no network mounts&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;outsideBootVolume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;standardized&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="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&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;Read top to bottom, that is the whole safety model. A candidate path has to survive all of it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Known target.&lt;/strong&gt; The cleanup target has to be one the app actually ships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No traversal.&lt;/strong&gt; Any path containing &lt;code&gt;..&lt;/code&gt; is rejected before it is even normalized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No symlinked leaf.&lt;/strong&gt; If the final component is a symlink, refuse it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a protected folder.&lt;/strong&gt; Documents, Desktop, Photos, Mail, iCloud, Keychains, and unnamed Application Support are out, even as parents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inside an allowlist root.&lt;/strong&gt; It must descend from a registered cache directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Still inside after resolving symlinks.&lt;/strong&gt; Resolve every link on both sides, then require it to still be inside that root.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On the boot volume.&lt;/strong&gt; Same volume as your home folder, checked with &lt;code&gt;statfs&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The two checks worth explaining
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Protected folders are rejected even as a parent.&lt;/strong&gt; Your home &lt;code&gt;Documents&lt;/code&gt;, &lt;code&gt;Desktop&lt;/code&gt;, &lt;code&gt;Pictures&lt;/code&gt;, Photos library, &lt;code&gt;Music&lt;/code&gt;, &lt;code&gt;Movies&lt;/code&gt;, &lt;code&gt;Mail&lt;/code&gt;, iCloud Drive, and &lt;code&gt;Keychains&lt;/code&gt; are blocked, not only on an exact match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;prohibitedPrefixes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"Documents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Desktop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Pictures"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Photos Library.photoslibrary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"Music"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Movies"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Mail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Mobile Documents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Keychains"&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;Application Support is the dangerous one, because it holds both throwaway caches and real, irreplaceable data: app databases, your actual messages, project files. So Dusty refuses all of &lt;code&gt;~/Library/Application Support&lt;/code&gt; unless the path ends in one specific, named cache subfolder, like &lt;code&gt;/Code/Cache&lt;/code&gt; or &lt;code&gt;/Slack/GPUCache&lt;/code&gt;. It will never take an app's whole Application Support directory, because that is where the data you cannot get back tends to live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symlinks get two passes.&lt;/strong&gt; The leaf check rejects a path that is itself a symlink. But normalizing a path does not resolve symlinks higher up, so a symlinked directory anywhere above the leaf, say a relocated &lt;code&gt;~/Library/Caches&lt;/code&gt;, could otherwise redirect a delete out of the allowlist. So the sixth check resolves symlinks fully on both the candidate and every allowlist root, then requires the resolved path to still live inside a resolved root. An ancestor symlink cannot smuggle a delete outside its box.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rules buy you in the UI
&lt;/h2&gt;

&lt;p&gt;Because the engine refuses anything off the list, the app does not need a "clean everything" button, and it does not have one. It scans, sizes every path, and shows you the list before it touches a thing. You can do a dry run. When you do delete, it can move files to the Trash instead of unlinking them, so there is an undo, and it writes a log of what it removed. The scary part of a cleaner is the part you cannot see. The point here is that there isn't one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;It will not find every last gigabyte. It cleans known caches and developer leftovers, not your 40 GB of forgotten video exports sitting in Documents, because reaching into Documents is the exact thing it refuses to do. It is not a disk visualizer and not an uninstaller. If you want an aggressive "reclaim everything" tool, this isn't it, on purpose.&lt;/p&gt;

&lt;p&gt;It is free, MIT licensed, signed and notarized. The engine and its tests are in the repo if you would rather check the claims than take my word for it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; yagcioglutoprak/tap/dusty
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code: &lt;a href="https://github.com/yagcioglutoprak/dusty" rel="noopener noreferrer"&gt;https://github.com/yagcioglutoprak/dusty&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If there is a cache you would want it to recognize that it does not yet, adding one is a single entry in the registry, so that is the easiest kind of contribution to take.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built an encyclopedia of 376 psychoactive substances with Next.js and PostgreSQL</title>
      <dc:creator>Toprak</dc:creator>
      <pubDate>Tue, 17 Mar 2026 00:39:21 +0000</pubDate>
      <link>https://dev.to/yagcioglutoprak/i-built-an-encyclopedia-of-376-psychoactive-substances-with-nextjs-and-postgresql-1cj7</link>
      <guid>https://dev.to/yagcioglutoprak/i-built-an-encyclopedia-of-376-psychoactive-substances-with-nextjs-and-postgresql-1cj7</guid>
      <description>&lt;p&gt;Finding reliable information about psychoactive substances shouldn't require 15 open tabs. PsychonautWiki for pharmacology, TripSit for interactions, Erowid for trip reports, Reddit for community knowledge — I wanted one place where all of that connects.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://substancewiki.org" rel="noopener noreferrer"&gt;SubstanceWiki&lt;/a&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;376 substances&lt;/strong&gt; with detailed profiles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15,015 interaction pairs&lt;/strong&gt; with safety ratings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;240 catalogued subjective effects&lt;/strong&gt; across 7 categories&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2,748 experience reports&lt;/strong&gt; from real users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;23,520 aggregated community posts&lt;/strong&gt; from Reddit and forums&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;21 combo guides&lt;/strong&gt; for common and high-risk combinations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;10 side-by-side substance comparisons&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every substance page includes dosage tables by route of administration, duration timelines, subjective effect profiles, interaction safety data, and community context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; with App Router and Turbopack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prisma + PostgreSQL&lt;/strong&gt; (hosted on Neon)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS + shadcn/ui&lt;/strong&gt; for the interface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; for deployment&lt;/li&gt;
&lt;li&gt;Dynamic sitemap generation, JSON-LD structured data on every page, and OG image support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The database schema has 11 models covering substances, effects, interactions, experience reports, articles, and community posts. Everything is cross-linked — substances link to their effects, effects link back to substances, interactions connect pairs with safety ratings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why harm reduction matters
&lt;/h2&gt;

&lt;p&gt;People use substances whether we like it or not. Giving them accurate, complete information is how we keep them safer. Every substance page leads with safety information because that's what matters most.&lt;/p&gt;

&lt;p&gt;The interaction checker alone covers 15,015 pairs — you can look up whether combining two substances is safe, risky, or dangerous in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check it out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://substancewiki.org" rel="noopener noreferrer"&gt;substancewiki.org&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd love feedback on what's missing, what's confusing, or what could be structured better. This is safety-critical information, so clarity matters more than aesthetics.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>postgres</category>
    </item>
    <item>
      <title>I built an open-source encyclopedia of psychoactive substances with Next.js 16</title>
      <dc:creator>Toprak</dc:creator>
      <pubDate>Mon, 09 Mar 2026 10:20:20 +0000</pubDate>
      <link>https://dev.to/yagcioglutoprak/i-built-an-open-source-encyclopedia-of-psychoactive-substances-with-nextjs-16-a0j</link>
      <guid>https://dev.to/yagcioglutoprak/i-built-an-open-source-encyclopedia-of-psychoactive-substances-with-nextjs-16-a0j</guid>
      <description>&lt;p&gt;Substance information is fragmented. You need PsychonautWiki for pharmacology, TripSit for interaction charts, Erowid for experience reports, Reddit for community knowledge. I got tired of having 15 tabs open, so I built &lt;strong&gt;SubstanceWiki&lt;/strong&gt; — a single, interconnected reference where everything links to everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live site:&lt;/strong&gt; &lt;a href="https://substancewiki.org" rel="noopener noreferrer"&gt;substancewiki.org&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/yagcioglutoprak/substance_wiki" rel="noopener noreferrer"&gt;github.com/yagcioglutoprak/substance_wiki&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;381&lt;/strong&gt; substance profiles with dosage guides and duration timelines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;14,967&lt;/strong&gt; drug interaction safety ratings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;240+&lt;/strong&gt; catalogued subjective effects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2,745&lt;/strong&gt; experience reports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;11,698&lt;/strong&gt; effect-substance combination pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~15,000&lt;/strong&gt; total pages generated&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; with App Router and Turbopack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; via Prisma (11 models)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; + shadcn/ui&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; for hosting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;@vercel/og&lt;/strong&gt; for dynamic OG images&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architectural decisions worth sharing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Pre-computed interactions, not runtime calculation
&lt;/h3&gt;

&lt;p&gt;The interaction checker covers 14,967 substance combinations. Each pair has a safety rating (Safe, Low Risk, Caution, Unsafe, Dangerous) with detailed notes about mechanism of action.&lt;/p&gt;

&lt;p&gt;These are pre-computed and stored in PostgreSQL — not calculated at runtime. A &lt;code&gt;SubstanceInteraction&lt;/code&gt; model with foreign keys to both substances, indexed for fast lookup in both directions. Querying "what interacts dangerously with LSD?" is a single indexed query returning in &amp;lt;10ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Programmatic SEO at scale
&lt;/h3&gt;

&lt;p&gt;From 381 substances and 240 effects, we generate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;381&lt;/strong&gt; main substance pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;762&lt;/strong&gt; dosage + effects sub-pages (&lt;code&gt;/substances/[slug]/dosage&lt;/code&gt;, &lt;code&gt;/substances/[slug]/effects&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;11,698&lt;/strong&gt; effect-substance detail pages (&lt;code&gt;/effects/[effect]/[substance]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;28&lt;/strong&gt; comparison pages (&lt;code&gt;/compare/lsd-vs-mdma&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;11&lt;/strong&gt; combo safety guides&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;56&lt;/strong&gt; editorial articles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each page has unique content, structured data (JSON-LD), dynamic OG images, and proper canonical URLs. The sitemap regenerates hourly with all 15,000+ URLs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Community insights from Reddit
&lt;/h3&gt;

&lt;p&gt;We process Reddit data via the Arctic Shift archive. Posts from relevant subreddits (r/drugs, r/psychonaut, r/nootropics, etc.) are classified, scored for quality, and linked to substance pages. This gives each substance a "community perspective" section with real discussions from people who've actually used it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Dynamic OG images
&lt;/h3&gt;

&lt;p&gt;Every page type gets a unique OG image generated via &lt;code&gt;@vercel/og&lt;/code&gt;. This means every link shared on social media has a distinct, informative preview — not a generic site image.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Internal linking as an SEO strategy
&lt;/h3&gt;

&lt;p&gt;Substance pages link to their effects. Effect pages link back to substances. Interactions connect pairs. Comparisons link both ways. This creates a dense web of internal links that helps both users and search engines understand the relationships between content.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. FAQ schema for rich snippets
&lt;/h3&gt;

&lt;p&gt;Each substance page dynamically generates FAQ content from the substance's data — "Is [substance] addictive?", "How long does [substance] last?", "What are the effects of [substance]?". This is wrapped in FAQPage JSON-LD schema, which can trigger FAQ rich snippets in Google search results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Harm reduction first
&lt;/h2&gt;

&lt;p&gt;Every substance page leads with safety information. The interaction checker exists because combining substances is one of the most dangerous things people do, and having that information one click away can save lives.&lt;/p&gt;

&lt;p&gt;This isn't a project that glorifies substance use. It's a project that acknowledges people use substances and tries to give them the best information possible to stay safe.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Multilingual support (Spanish, Portuguese, German)&lt;/li&gt;
&lt;li&gt;User-contributed experience reports&lt;/li&gt;
&lt;li&gt;Mobile app&lt;/li&gt;
&lt;li&gt;API for third-party integrations&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Check it out at &lt;a href="https://substancewiki.org" rel="noopener noreferrer"&gt;substancewiki.org&lt;/a&gt; and the source at &lt;a href="https://github.com/yagcioglutoprak/substance_wiki" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Feedback welcome — especially on accuracy of substance data, since getting this right is literally a matter of safety.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>database</category>
    </item>
    <item>
      <title>I built an open-source encyclopedia of psychoactive substances with Next.js 16 — here's what I learned</title>
      <dc:creator>Toprak</dc:creator>
      <pubDate>Sun, 08 Mar 2026 09:35:35 +0000</pubDate>
      <link>https://dev.to/yagcioglutoprak/i-built-an-open-source-encyclopedia-of-psychoactive-substances-with-nextjs-16-heres-what-i-2o2c</link>
      <guid>https://dev.to/yagcioglutoprak/i-built-an-open-source-encyclopedia-of-psychoactive-substances-with-nextjs-16-heres-what-i-2o2c</guid>
      <description>&lt;p&gt;Substance information is fragmented. You need PsychonautWiki for pharmacology, TripSit for interaction charts, Erowid for experience reports, Reddit for community knowledge. I got tired of having 15 tabs open, so I built &lt;strong&gt;SubstanceWiki&lt;/strong&gt; — a single, interconnected reference where everything links to everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live site:&lt;/strong&gt; &lt;a href="https://substancewiki.org" rel="noopener noreferrer"&gt;substancewiki.org&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/yagcioglutoprak/substance_wiki" rel="noopener noreferrer"&gt;github.com/yagcioglutoprak/substance_wiki&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;381&lt;/strong&gt; substance profiles with dosage guides and duration timelines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;14,967&lt;/strong&gt; drug interaction safety ratings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;240+&lt;/strong&gt; catalogued subjective effects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2,745&lt;/strong&gt; experience reports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;11,698&lt;/strong&gt; effect-substance combination pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~15,000&lt;/strong&gt; total pages generated&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; with App Router and Turbopack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; via Prisma (11 models)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; + shadcn/ui&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; for hosting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;@vercel/og&lt;/strong&gt; for dynamic OG images&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architectural decisions worth sharing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Pre-computed interactions, not runtime calculation
&lt;/h3&gt;

&lt;p&gt;The interaction checker covers 14,967 substance combinations. Each pair has a safety rating (Safe, Low Risk, Caution, Unsafe, Dangerous) with detailed notes about mechanism of action.&lt;/p&gt;

&lt;p&gt;These are pre-computed and stored in PostgreSQL — not calculated at runtime. A SubstanceInteraction model with foreign keys to both substances, indexed for fast lookup in both directions. Querying "what interacts dangerously with LSD?" is a single indexed query returning in &amp;lt;10ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Programmatic SEO at scale
&lt;/h3&gt;

&lt;p&gt;From 381 substances and 240 effects, we generate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;381&lt;/strong&gt; main substance pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;762&lt;/strong&gt; dosage + effects sub-pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;11,698&lt;/strong&gt; effect-substance detail pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;28&lt;/strong&gt; comparison pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;11&lt;/strong&gt; combo safety guides&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;56&lt;/strong&gt; editorial articles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each page has unique content, structured data (JSON-LD), dynamic OG images, and proper canonical URLs. The sitemap regenerates hourly with all 15,000+ URLs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Community insights from Reddit
&lt;/h3&gt;

&lt;p&gt;We process Reddit data via the Arctic Shift archive. Posts from relevant subreddits (r/drugs, r/psychonaut, r/nootropics, etc.) are classified, scored for quality, and linked to substance pages. This gives each substance a "community perspective" section with real discussions.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Dynamic OG images
&lt;/h3&gt;

&lt;p&gt;Every page type gets a unique OG image generated via @vercel/og. This means every link shared on social media has a distinct, informative preview — not a generic site image.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Internal linking as an SEO strategy
&lt;/h3&gt;

&lt;p&gt;Substance pages link to their effects. Effect pages link back to substances. Interactions connect pairs. Comparisons link both ways. This creates a dense web of internal links that helps both users and search engines understand the relationships between content.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. FAQ schema for rich snippets
&lt;/h3&gt;

&lt;p&gt;Each substance page dynamically generates FAQ content from the substance's data. This is wrapped in FAQPage JSON-LD schema, which can trigger FAQ rich snippets in Google search results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Harm reduction first
&lt;/h2&gt;

&lt;p&gt;Every substance page leads with safety information. The interaction checker exists because combining substances is one of the most dangerous things people do, and having that information one click away can save lives.&lt;/p&gt;

&lt;p&gt;This isn't a project that glorifies substance use. It's a project that acknowledges people use substances and tries to give them the best information possible to stay safe.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Multilingual support (Spanish, Portuguese, German)&lt;/li&gt;
&lt;li&gt;User-contributed experience reports&lt;/li&gt;
&lt;li&gt;Mobile app&lt;/li&gt;
&lt;li&gt;API for third-party integrations&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Check it out at &lt;a href="https://substancewiki.org" rel="noopener noreferrer"&gt;substancewiki.org&lt;/a&gt; and the source at &lt;a href="https://github.com/yagcioglutoprak/substance_wiki" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Feedback welcome — especially on accuracy of substance data, since getting this right is literally a matter of safety.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>database</category>
    </item>
    <item>
      <title>I built a macOS menu bar app in ~900 lines of Python that tracks Claude + ChatGPT limits — here's how</title>
      <dc:creator>Toprak</dc:creator>
      <pubDate>Thu, 26 Feb 2026 10:18:40 +0000</pubDate>
      <link>https://dev.to/yagcioglutoprak/i-built-a-macos-menu-bar-app-in-900-lines-of-python-that-tracks-claude-chatgpt-limits-heres-5he7</link>
      <guid>https://dev.to/yagcioglutoprak/i-built-a-macos-menu-bar-app-in-900-lines-of-python-that-tracks-claude-chatgpt-limits-heres-5he7</guid>
      <description>&lt;p&gt;I got tired of getting blindsided by Claude Pro rate limits mid-session. The solution: a menu bar app that reads the same private API that claude.ai/settings/usage calls.&lt;/p&gt;

&lt;p&gt;Here's what made it interesting to build:&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cloudflare problem
&lt;/h2&gt;

&lt;p&gt;Claude's API is behind Cloudflare. Regular &lt;code&gt;requests&lt;/code&gt; or &lt;code&gt;httpx&lt;/code&gt; get 403'd immediately. The fix: &lt;code&gt;curl_cffi&lt;/code&gt; with &lt;code&gt;impersonate="chrome131"&lt;/code&gt; to spoof the TLS fingerprint. This was the single biggest hurdle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser cookie detection
&lt;/h2&gt;

&lt;p&gt;Rather than making users copy-paste cookies, I used &lt;code&gt;browser-cookie3&lt;/code&gt; to read them directly from Chrome, Arc, Firefox, Safari, or Brave. Key insight: &lt;strong&gt;try Firefox first&lt;/strong&gt; — it uses a plain SQLite database with no Keychain prompt. Chromium browsers need a one-time "Always Allow" dialog.&lt;/p&gt;

&lt;h2&gt;
  
  
  AppKit threading rule
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;rumps&lt;/code&gt; runs on the main thread. All UI updates (menu rebuilds) must happen there. My background fetch thread puts results in a &lt;code&gt;queue.Queue&lt;/code&gt;, and a 0.25-second timer on the main thread drains it. Violate this and you get intermittent crashes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inconsistent API scale
&lt;/h2&gt;

&lt;p&gt;The API returns &lt;code&gt;five_hour&lt;/code&gt; (session) as a 0–1 fraction, but &lt;code&gt;seven_day&lt;/code&gt; (weekly) as a 0–100 percentage. The fix: &lt;code&gt;if raw &amp;gt; 1.0: already_pct else multiply_by_100&lt;/code&gt;. Spent embarrassing time debugging this.&lt;/p&gt;

&lt;h2&gt;
  
  
  One-line install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/yagcioglutoprak/AIQuotaBar/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also tracks ChatGPT usage in the same menu bar indicator.&lt;/p&gt;

&lt;p&gt;MIT licensed, ~900 lines Python: &lt;a href="https://github.com/yagcioglutoprak/AIQuotaBar" rel="noopener noreferrer"&gt;https://github.com/yagcioglutoprak/AIQuotaBar&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to go deep on any of these implementation details in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
