If you allow user-generated content in your app, you eventually run into a massive liability problem. Users will upload NSFW images.
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.
Usually, the process goes like this:
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.
To solve this, I built a single Node.js endpoint that runs the entire classification in memory.
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.
Here is the core logic using the open-source nsfwjs
library and TensorFlow.js:
const express = require('express');
const multer = require('multer');
const tf = require('@tensorflow/tfjs-node');
const nsfwjs = require('nsfwjs');
const app = express();
// Keep the file in memory, do not write to disk
const upload = multer({ storage: multer.memoryStorage() });
let _model;
const loadModel = async () => {
_model = await nsfwjs.load();
};
app.post('/moderate', upload.single('image'), async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No image provided' });
}
try {
// 1. Decode the image buffer directly from memory
const imageTensor = tf.node.decodeImage(req.file.buffer, 3);
// 2. Pass the tensor to the model
const predictions = await _model.classify(imageTensor);
// 3. Destroy the tensor to free memory immediately
imageTensor.dispose();
// 4. Return the scores
return res.json(predictions);
} catch (error) {
return res.status(500).json({ error: 'Processing failed' });
}
});
loadModel().then(() => app.listen(3000));
1. Total Privacy. Because we use multer.memoryStorage()
, 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.
2. Speed. Bypassing the file system completely keeps the classification extremely fast. In my testing, it drops to roughly 200ms per image on a standard VPS.
3. Simplicity. It is a synchronous API call. You can run this check before your database transaction even commits, keeping your backend architecture clean.
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 .dispose()
.
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 Tabu 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.
Let me know how you guys handle image moderation in your own side projects!