Mobil cihazlarda 5G’yi aktif hale getirme [Nasıl yapılır?]

5G

5GTürkiye’de mobil internet deneyiminde yeni bir dönem başlıyor. 1 Nisan 2026 itibarıyla ülke genelinde 5G hizmeti resmi olarak aktif hale gelecek. 5G, 4,5G’ye göre çok daha yüksek hız ve düşük gecikme süresi sunarak video akışlarından online oyunlara, bulut uygulamalardan akıllı cihazlara kadar pek çok alanda fark yaratacak. Yeni nesil mobil bağlantıdan yararlanmak için hem cihazınızın hem de operatörünüzün 5G desteği sunması gerekiyor. 5G uyumlu telefonlar ve SIM kartlar, kullanıcıların bu yüksek hızdan anında faydalanmasını mümkün kılıyor. Telefonunuzun 5G desteği olup olmadığını kontrol edin. Yeni çıkan Android modeller ve iPhone’ların büyük kısmı 5G’yi destekliyor. Ayarlardan 5G’yi Açın Android için: Ayarlar – Bağlantılar – Mobil Ağ – Ağ Modu- ”5G/4G/3G/2G (otomatik)” seçeneğini işaretleyin. iPhone için: Ayarlar – Hücresel – Hücresel Veri Seçenekleri – Ses ve Veri- ”5G Açık” veya ”5G Otomatik” seçeneğini aktif edin. 5G sinyali Kontrol Edin Telefonunuzda “5G” işaretini görmeniz, bağlantının yeni nesil 5G altyapısı üzerinden gerçekleştiğini gösterir. 5G teknolojisinin […]

Mobil cihazlarda 5G’yi aktif hale getirme [Nasıl yapılır?] yazısı ilk olarak Teknoloji haberleri – LOG üzerinde yayınlandı.

Deploy Apps to Samsung Smart TV: The Bash Script That Saves Hours

Chrome DevTools Devices page with Configure button highlighted

If you’ve ever deployed a web app to a Samsung Smart TV, you know the pain. Connect via sdb, resolve the device name, package the .wgt file with the right certificate, uninstall the old version, install the new one, run it, pray it doesn’t crash. Every. Single. Time.

After 9+ years of doing this across dozens of OTT projects — including platforms serving 80M+ viewers — I finally snapped and wrote a script that automates the entire thing.

I’m sharing it here because I wish someone had shared something like this with me years ago.

What You Need Before Starting

Before the script can do its magic, you need three things set up on your machine and your TV.

1. Install Tizen Studio. Download it from the official Tizen Studio page. You need the CLI tools — specifically sdb and the tizen command-line utility. The script expects them at ~/tizen-studio/tools/. After the base installation, open Package Manager, go to the Extension SDK tab, and install TV Extensions and Samsung Certificate Extension. The full process is described in the Samsung TV SDK installation guide.

2. Create a Samsung certificate. This is the part that trips most people up. You need a signing certificate to package and install apps on a physical TV. Follow the Samsung certificate guide: open Certificate Manager in Tizen Studio, create a Samsung certificate profile (not a Tizen one), authenticate with your Samsung Developer account, and register your TV’s DUID as the target device. Keep the certificate backed up — future updates must use the same author certificate or the TV treats them as a completely new app.

3. Enable Developer Mode on the TV. On the TV itself, go to the Apps panel, open App Settings, and enter the code 12345. This opens the Developer Mode popup. Toggle it on, enter your computer’s IP address, and reboot the TV. After restart you’ll see “Develop Mode” at the top of the Apps panel. The full walkthrough is in the Samsung device setup guide.

Also required: Java 8. Tizen CLI tools still depend on Java 8, not newer versions. On macOS, install it with brew install --cask temurin8. The script automatically detects and switches to Java 8 for the session, so it won’t mess with your system Java.

What the Script Does

The script gives you an interactive menu powered by gum (a terminal UI toolkit that it auto-installs if missing). You get 8 options:

  1. Run All Steps — connect, resolve device, package, uninstall old version, install, and run. One selection, done.
  2. Connect to TV — establishes sdb connection to your TV’s IP.
  3. Resolve device name — reads the device identifier from sdb, needed for all Tizen CLI commands.
  4. Package app — signs your build directory as a .wgt file with your certificate. Handles filename sanitization automatically.
  5. Uninstall app — removes the existing version from the TV (gracefully ignores if not installed).
  6. Install app — pushes the .wgt to the TV.
  7. Debug app — launches a debug session and forwards the debug port so you can attach Chrome DevTools.
  8. Run app — launches the app in non-debug mode.

You can also pass a pre-built .wgt file directly, and the script skips the packaging step entirely.

Config Persistence: Set Once, Deploy Forever

On first run, the script asks you four things: TV IP address, certificate name, package ID, and the path to your build directory (or .wgt file). The package ID comes from your project’s config.xml — it’s the combination of the tizen:application package attribute, in the format AbCdEf1234.MyApp. The script saves all of this to ~/.tizen_deploy_config, so every subsequent run just works — no re-entering values.

Working on a different project or switched TVs? Run the script with --clear-config to reset everything:

./tizen_deploy.sh --clear-config

This wipes the saved config and prompts you fresh.

The Full Script

Here’s the complete source. Save it as tizen_deploy.sh, make it executable with chmod +x tizen_deploy.sh, and run it.

#!/bin/bash

# PLATFORM DETECTION
OS_TYPE="$(uname -s)"
IS_MAC=false
IS_LINUX=false
IS_WIN=false

case "$OS_TYPE" in
  Darwin*) IS_MAC=true ;;
  Linux*) IS_LINUX=true ;;
  MINGW*|MSYS*|CYGWIN*) IS_WIN=true ;;
esac

# CONFIG PATH
CONFIG_PATH="$HOME/.tizen_deploy_config"
$IS_WIN && CONFIG_PATH="$HOME/_tizen_deploy_config"

# --clear-config ARG HANDLING
if [[ "$1" == "--clear-config" ]]; then
  echo "Clearing config at $CONFIG_PATH"
  rm -f "$CONFIG_PATH"
fi

# INSTALL GUM IF NOT FOUND
install_gum() {
  if command -v gum &>/dev/null; then return; fi
  echo "'gum' is not installed. Installing..."

  if $IS_MAC; then
    if ! command -v brew &>/dev/null; then
      echo "Homebrew is required on macOS." >&2
      exit 1
    fi
    brew install charmbracelet/tap/gum
  elif $IS_LINUX; then
    if command -v apt &>/dev/null; then
      sudo apt update && sudo apt install -y gum
    elif command -v dnf &>/dev/null; then
      sudo dnf install -y gum
    elif command -v pacman &>/dev/null; then
      sudo pacman -Sy gum
    else
      echo "Unsupported package manager." >&2
      exit 1
    fi
  elif $IS_WIN; then
    echo "On Windows, install gum manually:"
    echo "  scoop install gum"
    read -p "Press Enter when installed..."
  fi
}

if ! command -v tizen &>/dev/null; then
  echo "Tizen CLI not found. Install Tizen Studio first."
  exit 1
fi

install_gum

# FORCE JAVA 8 FOR TIZEN
if command -v /usr/libexec/java_home &>/dev/null; then
  JAVA_8_HOME=$(/usr/libexec/java_home -v 1.8 2>/dev/null)
  if [[ -z "$JAVA_8_HOME" ]]; then
    echo "Java 8 is required. Install: brew install --cask temurin8"
    exit 1
  fi
  export JAVA_HOME="$JAVA_8_HOME"
  export PATH="$JAVA_HOME/bin:$PATH"
fi

# LOAD / PROMPT CONFIGURATION
prompt_or_load() {
  local var_name=$1
  local prompt_msg=$2
  local current_value=$(grep "^$var_name=" "$CONFIG_PATH" 2>/dev/null | cut -d'=' -f2-)
  if [ -n "$current_value" ]; then
    eval "$var_name="$current_value""
  else
    read -p "$prompt_msg: " user_input
    echo "$var_name="$user_input"" >> "$CONFIG_PATH"
    eval "$var_name="$user_input""
  fi
}

prompt_or_load TV_IP "Enter TV IP address"
prompt_or_load CERTIFICATE_NAME "Enter Certificate/Profile name"
prompt_or_load PACKAGE_ID "Enter PACKAGE_ID (e.g., abc123.MyApp)"
prompt_or_load INPUT_PATH "Enter build dir or .wgt file path"

# Derive behavior based on INPUT_PATH
if [[ "$INPUT_PATH" == *.wgt ]]; then
  BUILD_DIR="$(dirname "$INPUT_PATH")"
  APP_NAME="$(basename "$INPUT_PATH")"
  SKIP_PACKAGE=true
else
  BUILD_DIR="$INPUT_PATH"
  APP_NAME="$(echo "$PACKAGE_ID" | cut -d. -f2).wgt"
  SKIP_PACKAGE=false
fi

SDB="$HOME/tizen-studio/tools/sdb"
TIZEN_CLI="$HOME/tizen-studio/tools/ide/bin/tizen"

TV_PORT="26101"
DEVICE_NAME=""
DEBUG_PORT=""

# STEP FUNCTIONS
connect() {
  echo "Connecting to $TV_IP:$TV_PORT..."
  CONNECT_OUTPUT=$($SDB connect "$TV_IP:$TV_PORT" 2>&1)
  echo "$CONNECT_OUTPUT" | grep -Eq "connected to|is already connected" 
    && echo "Connected." || { echo "Failed."; echo "$CONNECT_OUTPUT"; exit 1; }
}

resolve_device() {
  DEVICE_NAME=$($SDB devices | grep "$TV_IP:$TV_PORT" | awk '{print $3}')
  [[ -z "$DEVICE_NAME" ]] && { echo "Could not resolve device."; exit 1; }
  echo "Device: $DEVICE_NAME"
}

package_app() {
  $SKIP_PACKAGE && { echo "Skipping packaging (.wgt provided)"; return; }
  PACKAGE_OUTPUT=$($TIZEN_CLI package -s "$CERTIFICATE_NAME" -t wgt -- "$BUILD_DIR" 2>&1)
  if echo "$PACKAGE_OUTPUT" | grep -q "Package File Location"; then
    PKG_FILE=$(echo "$PACKAGE_OUTPUT" | grep "Package File Location" | sed -E 's/.*:s*//' | xargs)
    NEW_BASENAME=$(basename "$PKG_FILE" | tr -d ' ')
    mv "$PKG_FILE" "$(dirname "$PKG_FILE")/$NEW_BASENAME" 2>/dev/null
    APP_NAME="$NEW_BASENAME"
    echo "Packaged: $APP_NAME"
  else
    echo "Packaging failed."; echo "$PACKAGE_OUTPUT"; exit 1
  fi
}

uninstall_app() {
  $TIZEN_CLI uninstall -t "$DEVICE_NAME" -p "$PACKAGE_ID" 2>&1 | 
    grep -q "uninstall completed" && echo "Uninstalled." || echo "Not installed, skipping."
}

install_app() {
  INSTALL_OUTPUT=$($TIZEN_CLI install --name "$APP_NAME" -t "$DEVICE_NAME" -- "$BUILD_DIR" 2>&1)
  echo "$INSTALL_OUTPUT" | grep -q "successfully installed" 
    && echo "Installed." || { echo "Install failed."; echo "$INSTALL_OUTPUT"; exit 1; }
}

debug_app() {
  DEBUG_OUTPUT=$($SDB shell 0 debug "$PACKAGE_ID" 2>&1)
  DEBUG_PORT=$(echo "$DEBUG_OUTPUT" | grep -oE "port: [0-9]+" | awk '{print $2}')
  [[ -z "$DEBUG_PORT" ]] && { echo "Debug failed."; echo "$DEBUG_OUTPUT"; exit 1; }
  $SDB forward "tcp:$DEBUG_PORT" "tcp:$DEBUG_PORT" 2>&1
  echo "Debug on port $DEBUG_PORT"
}

run_app() {
  RUN_OUTPUT=$($TIZEN_CLI run -t "$DEVICE_NAME" -p "$PACKAGE_ID" 2>&1)
  echo "$RUN_OUTPUT" | grep -q "successfully launched" 
    && echo "Launched." || { echo "Launch failed."; echo "$RUN_OUTPUT"; exit 1; }
}

run_all() { connect; resolve_device; package_app; uninstall_app; install_app; run_app; }

# INTERACTIVE MENU
choice=$(gum choose 
  "Run All Steps (1-5, 7)" 
  "1. Connect to TV" 
  "2. Resolve device name" 
  "3. Package app" 
  "4. Uninstall app" 
  "5. Install app" 
  "6. Debug app + forward port" 
  "7. Run app (non-debug)" 
  "8. Disconnect from TV")

case "$choice" in
  "Run All Steps (1-5, 7)") run_all ;;
  "1. Connect to TV") connect ;;
  "2. Resolve device name") resolve_device ;;
  "3. Package app") package_app ;;
  "4. Uninstall app") resolve_device && uninstall_app ;;
  "5. Install app") resolve_device && install_app ;;
  "6. Debug app + forward port") resolve_device && debug_app ;;
  "7. Run app (non-debug)") resolve_device && run_app ;;
  "8. Disconnect from TV") disconnect ;;
  *) echo "Invalid selection." && exit 1 ;;
esac

echo "Done."

How to Debug Your App with Chrome DevTools

One of the most useful features of the script is remote debugging. Here’s how to connect Chrome DevTools to your app running on the TV.

Step 1: Close the app on the TV. If the app is already running, close it first. The debug session needs to launch the app itself — it won’t attach to an already running instance.

Step 2: Run the debug command. Launch the script and select “Debug app + forward port” from the menu. The script will output a port number — copy it.

Step 3: Open Chrome DevTools for devices. In Chrome, go to chrome://inspect/#devices. Make sure “Discover network targets” is checked, then click Configure…

Chrome DevTools Devices page with Configure button highlighted

Step 4: Add the debug port. In the Target discovery settings dialog, add localhost:{port} where {port} is the number the script returned. Click Done.

Target discovery settings with debug port added

Step 5: Inspect. Your TV app should appear under Remote Target. Click inspect on the first item in the list — a full Chrome DevTools window opens, connected to your app on the TV. You can inspect DOM, debug JavaScript, profile performance, and see network requests just like you would on a regular web page.

Remote Target showing the TV app with inspect link highlighted

Important Notes and Limitations

A few things to keep in mind:

  • Tested on macOS. The script has platform detection for Linux and Windows (Git Bash), but I’ve only battle-tested it on macOS. Linux should work with minor adjustments. Windows via Git Bash/MSYS is experimental — your mileage may vary.
  • Java 8 is mandatory. Tizen CLI refuses to work with Java 11+. The script handles this by temporarily switching JAVA_HOME for the session only — your system Java stays untouched.
  • Certificate issues are the #1 problem. If install fails with a signing error, check three things: the certificate profile name must match exactly, the TV’s DUID must be registered in the distributor certificate, and the certificate type matters — you need a Samsung certificate (not Tizen), and the privilege level must be correct (Partner for apps using privileged APIs, Public for basic apps).
  • gum is auto-installed. The interactive menu uses gum from Charm. If it’s not installed, the script installs it via Homebrew (macOS), apt/dnf/pacman (Linux), or asks you to install it manually (Windows).

About Me

I’m Aleksandr Sakov — a full-stack developer with 9+ years of experience building OTT streaming platforms, mobile apps, and web products. The platforms I’ve built serve 80M+ viewers across 15+ device types including Samsung Tizen, LG WebOS, Android TV, Roku, and more.

If you’re building a Smart TV app or any streaming product and want someone who’s already solved the hard problems — book a free 30-minute call or try the project calculator for a quick estimate.

sundr.dev | LinkedIn

Courts Will Soon Judge Your Face Match Workflow, Not Just Your Results

How regulatory shifts are rewriting the biometric dev’s roadmap

The regulatory landscape for computer vision is shifting from “does it work?” to “is it defensible?” Brazil’s recent Digital ECA guidelines and the impending enforcement of the EU AI Act are moving the goalposts for developers working with biometric data. If you are building or maintaining facial comparison tools, the “Definition of Done” for your next sprint just got much more complex. It is no longer enough to return a similarity score; you must now return an audit trail.

For developers working with facial comparison, the technical implications are immediate. We have historically focused on optimizing the Euclidean distance analysis—the mathematical backbone that measures the similarity between two facial embeddings in N-dimensional space. However, as synthetic media becomes more sophisticated, a low Euclidean distance score is no longer a definitive result; it is a potential liability. If your API returns a high-confidence match without first verifying that the source image isn’t a GAN-generated deepfake, your software is creating a forensic blind spot for the investigator using it.

The Shift from Matching to Provenance

In the next 24 months, the “standard” biometric pipeline will likely evolve. We are moving away from a simple two-step process (Input -> Match) toward a multi-stage auditable workflow:

  1. Input Validation: Checking for synthetic artifacts or deepfake markers before the embedding is even generated.
  2. Euclidean Analysis: Running the core comparison logic to determine side-by-side similarity.
  3. Metadata Logging: Stamping the result with a legal basis flag and a timestamped audit log.

As engineers, this means our JSON responses need to do more than just spit out a confidence float. We need to build systems that can reconstruct every decision point in the chain of analysis. If a private investigator presents your tool’s results in court, can your database prove why that match was made and that the source material was verified as real?

Scaling Enterprise Logic for the Solo Investigator

At CaraComp, we prioritize facial comparison—the targeted, side-by-side analysis of specific photos within a case—rather than mass surveillance. The technical challenge we’ve solved is delivering that same high-level Euclidean distance analysis used by federal agencies at a fraction of the cost. For the solo investigator, the friction isn’t usually the math; it’s the accessibility and the reporting.

We’ve focused on building batch processing capabilities that allow users to upload once and compare many, but the real “feature” is the court-ready report. From a development perspective, this means our backend isn’t just a comparison engine; it’s a document generator that translates complex algorithmic outputs into a format that a judge or an insurance adjuster can understand.

Why Your Codebase Needs a “Legal Basis” Layer

Whether you are using TensorFlow, PyTorch, or a custom wrapper, you need to start thinking about “compliance-by-design.” Brazil’s $9.4 million fine potential for non-compliance isn’t just a business risk; it’s a technical requirement. Your database schema should already be accounting for fields like consent_status and media_provenance.

The investigators who win cases in 2028 will be the ones whose tools didn’t just find the match, but proved the process. As developers, it is our job to ensure the math holds up under cross-examination.

How are you handling deepfake pre-processing in your computer vision pipelines—are you integrating it as a gated check or as an asynchronous metadata flag in your results?

Designing a Practical IIoT Architecture

Introduction

Industrial IoT (IIoT) architectures often look clean in diagrams but become messy when implemented in real systems.

While exploring this space, I tried to structure a more practical reference architecture that balances clarity with real-world concerns like scalability, reliability, and multi-tenancy.

This is a work in progress, and I’d love feedback from others building similar systems.

The Problem with Typical IoT Architectures

Most reference architectures:

  • stay too high-level
  • don’t separate concerns clearly (telemetry vs command)
  • ignore multi-tenancy
  • don’t address edge vs cloud trade-offs

This creates challenges when moving from POC → production.

Proposed Architecture Overview

At a high level, the system is divided into four layers:

1. Device & Controller Layer

  • Sensors, controllers, PLCs
  • Communicate via MQTT or similar lightweight protocols
  • Separate channels for telemetry and command

2. Edge Layer

  • Local processing and filtering
  • Handles intermittent connectivity
  • Reduces latency for critical operations

3. Cloud Ingestion Layer

  • MQTT broker → event streaming pipeline
  • Separation of:
    • telemetry (data ingestion)
    • command (control path)

4. Application Layer

  • Multi-tenant SaaS architecture
  • Monitoring dashboards
  • Command and control APIs

Key Design Considerations

Telemetry vs Command Separation

Combining both leads to complexity. Keeping them separate improves scalability and clarity.

Edge vs Cloud Responsibilities

  • Edge: real-time, low-latency decisions
  • Cloud: aggregation, analytics, orchestration

Multi-Tenancy

Needs to be built-in from the start (not bolted on later).

Open Questions / Trade-offs

Some areas I’m still exploring:

  • How to design reliable command execution (ACK/NACK, retries, idempotency)
  • Where to draw the boundary between edge and cloud
  • Best approaches for tenant isolation at scale

Reference

I’ve put together a more detailed version (with diagrams) here:

👉 https://gauravs19.github.io/iiot-reference-architecture/

Looking for Feedback

If you’ve worked on IoT/IIoT systems:

  • What patterns have worked well for you?
  • Where do architectures typically break?
  • What would you change here?

Open to suggestions and alternative approaches.

Two-level concurrency in Node.js – worker threads and async pools for data integration pipelines

Diagram: Simple linear pipeline - download file, apply mapping per row, save result

The problem

Imagine we develop a SaaS platform that analyzes users’ activity data – think fraud detection, behavioral analytics, compliance reporting. Our task is to create the integration pipeline that ingests and normalizes raw client data before it reaches those systems.
Our clients collect data, store it in CSV files, and upload them to their S3 buckets. Then they create a mapping for their data in our application – select a datatype for each field, some of them can be serialized JSON objects, others parsed with complex regular expressions or base64 decoded, etc. Our task is to download these files, parse each row, apply custom mapping, find corrupted records, and store the converted data on our side.

From a bird’s-eye view the whole integration task is nothing else but:

Diagram: Simple linear pipeline - download file, apply mapping per row, save result

In more detail it works like this:

  • Client stores CSV files in the S3 bucket. Each row represents one item
  • Client gives us credentials for accessing these files
  • Client creates mapping for rows
  • Client schedules integration runs

Diagram: Client owns S3 bucket with CSV files, gives credentials, describes integration mapping rules, and schedules the integration pipeline

When scheduled time comes we execute the integration performing the next steps:

  • push task to a queue
  • receive task by the integration pipeline
  • download file from client’s S3
  • get mapping
  • apply mapping per row
  • store new file to our S3 for further processing (uploading to the database, used by analytics tools, etc.)

Diagram: Integration pipeline downloads CSV from client's S3 using credentials, gets mapping rules, creates mapping result, and stores it to our S3 bucket

Code is:

const rawData = await downloadFile(file.url);
const processed = applyMapping(rawData, clientMapping);
await saveToS3(file.s3Key, processed);

Introducing a message queue (SQS, RabbitMQ, any) helps with a few things:

  • pure asynchronous execution
  • queue’s messages can be dispatched between any number of consumers so scaling is flexible
  • acknowledgment mechanism of queue can be utilized for managing retries
  • queues are great for managing backpressure

Message in queue can be something like this:

{
    "clientConnection": { ...S3Credentials, bucketName, etc. },
    "mappingId": ...
    "pathToCSV": ...
}

At this point we observe two issues:

  • if file size is relatively big (> 100 MB) we are starting to abuse RAM;
  • we split the processing into distinct phases (download, map, upload) which are sequential, and that increases execution time.

But both issues are quite easy to fix – we just need to introduce streams and process data chunk by chunk, row by row.
Streams are good for two reasons:

  • they allow us to keep memory usage flat by processing data chunk-by-chunk. All blob storages have streaming APIs as well as many HTTP-based services;
  • they handle backpressure automatically and data flows from source to destination through mapping without any issues even if there are some network delays or CPU-intensive processing.

Code is nothing more than:

import { pipeline } from 'node:stream/promises';
import { createDownloadStream } from './download';
import { createMappingTransform } from './mapping';
import { createS3UploadStream } from './upload';

await pipeline(
  createDownloadStream(file.url),
  createMappingTransform(clientMapping),
  createS3UploadStream(file.s3Key),
);

And that’s it. Many production systems work fine with this simple architecture. For sure, a few more things should be added like failure handling and reporting results, but overall it is a valid way to build a typical integration pipeline.

We delivered the integration code packaged as AWS Lambda and started monitoring the system.

Problems started to show up.

The first issue arose when the integration started to fail because some client’s CSV files were really big and it took a lot of time just to transfer gigabytes of data over the network. Lambdas had limited execution time and we just didn’t fit within this timeframe.

We made a decision to ship our code as a regular service to good old EC2 instances.

It worked fine until…

The second issue arose when one client requested batch processing. Their CSV files were huge and they had split them into multiple. They wanted the integration to download all of them, but treat the whole bunch as one logical unit.
Our intuition told us that we could just process all files in parallel and combine the execution results:

const results = await Promise.all(
  files.map(async file => {
    await pipeline(
      createDownloadStream(file.url),
      createMappingTransform(clientMapping),
      createS3UploadStream(file.s3Key),
    );
    return { fileId: file.id, status: 'ok' };
  })
);

It worked great because for each file most of the time V8 engine was in the idle state – waiting for network responses – so we could process multiple files in parallel without increasing processing time.

But then we had clients with a huge number of fields that required some parsing: JSON strings, base64 encoded text, complex regular expressions. The monitoring showed that processing time was increasing and Node.js process started to use noticeable CPU time.
The worst thing was that we were paying for a multicore EC2 instance but Node.js process was struggling using only one core.

Where Node.js gets stuck

To understand what’s going on we have to set our business logic aside for a bit and think how Node.js handles CPU-intensive tasks.

We all know that Node.js is single-threaded but it’s only partially true.
First of all let’s recall that Node.js consists of three parts:

  • V8 – JavaScript engine responsible for memory management (Heap, Garbage Collector) and code execution (Call Stack)
  • libuv – asynchronous operations handler (Event loop, thread pool, async I/O)
  • Node.js API – bridge between V8 and libuv

Our application’s JS code (business logic) is executed by V8 which is single-threaded and shines for I/O operations when all the heavy lifting is done by libuv and its C/C++ modules.

Libuv maintains a thread pool (configurable via UV_THREADPOOL_SIZE). Many standard Node.js modules – zlib, crypto, file system operations – delegate work to this pool via C/C++ libraries. So, even if we perform some heavy encryption or compression, V8’s event loop stays free.

Timeline: libuv thread pool handles I/O while the main thread event loop stays free for other work

If a task requires only I/O operations without loading the CPU, Node.js already handles it well. Downloading and uploading multiple files in parallel is done by libuv and doesn’t make the event loop busy, so for regular cases wrapping pipelines into Promise.all() would be enough.
However, our issue is rooted in the fact that we are going to perform CPU-intensive mapping on the V8 side and it blocks the single thread. Libuv can’t help us with this because there are no native addons for our business logic.

Timeline: libuv pool sits idle while CPU-intensive mapping blocks the main thread

And here we have the core question – how to optimize our pipeline for CPU-intensive operations and free up the main thread?

The obvious fix that doesn’t help

Node.js provides a few built-in ways to execute CPU-intensive tasks outside the main thread:

  • Worker Threads
  • Clusters
  • Child Processes (the low-level tool used by both above)

We will consider using Worker Threads as the best choice for our goals.

Worker Thread is a mechanism that allows creating a separate V8 isolate with its own event loop in a separate OS thread. The great thing about it is that the main process can pass the data to each Worker Thread, and on top of that, Worker Threads can send the data back to the main thread as well as notify it about errors and finishing of execution.

The simplest example of the integration pipeline execution can be:

import { Worker, isMainThread, workerData, parentPort } from 'node:worker_threads';
import { pipeline } from 'node:stream/promises';
import { createDownloadStream } from './download';
import { createMappingTransform } from './mapping';
import { createS3UploadStream } from './upload';

// This file runs twice: once as the main thread, once inside the worker.
// isMainThread tells us which context we're in.
if (isMainThread) {
  function runWorker(file: File, mapping: Mapping): Promise<void> {
    return new Promise((resolve, reject) => {
      // Spawns a new worker thread running this same file.
      // workerData is how we pass data into the worker at startup.
      const worker = new Worker(__filename, {
        workerData: { file, mapping }
      });

      // Workers communicate back via messages and error events
      worker.on('message', resolve);
      worker.on('error', reject);
    });
  }

  await runWorker(file, clientMapping);

} else {
  // Inside the worker thread: workerData contains what we passed in above
  const { file, mapping } = workerData;

  try {
    await pipeline(
      createDownloadStream(file.url),
      createMappingTransform(mapping),
      createS3UploadStream(file.s3Key),
    );
    // Signal the main thread that we're done
    parentPort!.postMessage('done');
  } catch (err) {
    throw err;
  }
}

For processing multiple files the naive solution is to create a worker per file:

import { Worker } from 'node:worker_threads';

await Promise.all(
  files.map(file => new Promise<void>((resolve, reject) => {
    const worker = new Worker('./pipeline.ts', {
      workerData: { file, mapping: clientMapping }
    });

    worker.on('message', resolve);
    worker.on('error', reject);
  }))
);

and pipeline.ts is:

import { workerData, parentPort } from 'node:worker_threads';

const { file, mapping } = workerData;

try {
  await pipeline(
    download(file.url),      // readable stream
    transform(mapping),      // transform stream  
    upload(file.s3Key),      // writable stream
  );
  parentPort!.postMessage('done');
} catch (err) {
  throw err;
}

However, this solution isn’t optimal if the number of open worker threads is higher than the number of CPU cores. For 8 files we start 8 workers but EC2 has only 4 cores – worker threads start to compete for cores and we get context switching overhead. However, it is not the worst part. The worst part is that each Worker Thread spins up a full V8 Isolate with its own heap and its own event loop. It might not be an issue if the number of files is low, but with high numbers the overhead becomes real.

Two-level concurrency

And here we are for building a final solution. Let’s first illustrate it with the next diagram:

Architecture diagram: Queue feeds Main Thread which distributes work to N worker threads (Level 1), each running M concurrent pipelines (Level 2)

As we discussed before, there is no need to create more Worker Threads than the number of CPU cores, so we can create exactly the same number N = num(CPU cores). It is our concurrency Level 1.
All N workers can be created during the service boot and wait for a task from the Main Thread.

If the number of files is higher than the number of workers N we can round-robin them between workers equally. Each worker then processes its assigned files through an async pool of M concurrent pipelines – this is our concurrency Level 2. While Level 1 spreads work across CPU cores, Level 2 keeps each core busy by overlapping I/O wait across multiple files within a single worker.

We can’t just Promise.all() the files – if a worker gets 25 files, that’s 25 pipelines fighting for memory and file descriptors at once. An async pool caps concurrency at M simultaneous pipelines per worker, starting the next one only when a slot frees up. When one pipeline waits for a network ACK, another executes CPU-intensive mapping and vice versa.

Let’s illustrate this process with a diagram for 6 files and 2 Worker Threads:

Timeline: two worker threads processing 6 files with interleaved network reads, CPU mapping, and network writes across concurrent pipelines

Notice that when one pipeline uses the CPU power for performing mapping, we still can download or upload data over HTTP – it is handled by libuv threads outside of the Worker Thread’s event loop.

As a result we can:

  • process CPU-heavy pipelines for multiple files with real CPU cores parallelization
  • utilize multicore server resources effectively
  • as files are processed in parallel, we can drastically reduce execution time
  • as parallel execution happens inside one Node.js process, orchestration and error handling would be much easier than if it were distributed between multiple processes or even machines

Keep in mind that each worker runs its own V8 isolate – that’s roughly 10–30MB of baseline memory per worker, regardless of workload. With N workers, that cost is part of the process’s total RSS. On a 4-core instance, you’re looking at 40–120MB just for the isolates before any actual data flows through.

When Main Thread pulls a scheduled integration run message from a queue it fetches a mapping, S3 credentials and spreads the work between Worker Threads. It waits until all workers report that the job succeeded and sends the ACK to the queue.
Files in a batch represent the same dataset – think of a single day’s activity split across multiple files for size reasons. If we ACK after processing only half of them, the queue considers the job done, but the downstream system gets an incomplete picture. For some clients, partial data is worse than no data at all. So the rule is simple: ACK fires only after every worker reports success. If anything fails, the message stays in the queue for a retry or lands in a dead-letter queue for investigation.

This is also why we keep a single batch inside a single process. Splitting one batch across multiple EC2 instances adds distributed coordination – every instance must agree on success, handle partial failures, and clean up after crashes. A single multi-core machine already handles the load. If multiple batches arrive at the same time, they queue up – each instance processes one batch at a time and picks up the next one when it’s done.

Since N always equals the number of CPU cores, you can size the EC2 instance based on how many files a client’s batch typically contains – a 2-core instance for small batches, a 16-core instance for large ones. No code changes required, and it could help optimize infrastructure costs.

Putting it together

The Main Thread will look something like this:

// (Main Thread)

const N = os.availableParallelism();

// Boot N workers once at process start
const workers = Array.from({ length: N }, () => new Worker('./worker.ts'));

// Listen for queue messages
queue.on('message', async (message) => {
  const { files, mappingId, credentials } = message;

  // Fetch mapping once - workers don't do this themselves
  const mapping = await mappingService.get(mappingId);

  // Distribute files across workers via round-robin
  const slices = roundRobin(files, N);

  // Dispatch each slice and wait for all workers to finish
  await Promise.all(
    workers.map((worker, i) =>
      dispatch(worker, { files: slices[i], mapping, credentials })
    )
  );

  // All workers done - safe to ACK
  queue.ack(message);
});

function dispatch(worker, payload): Promise<void> {
  return new Promise((resolve, reject) => {
    worker.postMessage(payload);
    worker.once('message', resolve);
    worker.once('error', reject);
  });
}

And Worker Thread will be updated with listening to a message from the Main Thread:

// (Worker Thread)

parentPort.on('message', async ({ files, mapping, credentials }) => {
  // Process assigned files as a pool of M concurrent pipelines
  // asyncPool(concurrency, items, fn) — runs at most M pipelines simultaneously 
  // (e.g. tiny-async-pool, or a custom implementation)
  await asyncPool(M, files, (file) =>
    pipeline(
      createDownloadStream(file.url, credentials),
      createMappingTransform(mapping),
      createS3UploadStream(file.s3Key),
    )
  );

  parentPort.postMessage({ status: 'done' });
});

This gives an idea how to start building CPU-bound integration pipelines avoiding computation bottleneck.

What this architecture does not solve (and why that’s fine)

I intentionally omit a few important parts that will be crucial for a production-ready implementation – they are out of scope of the article and highly depend on your current setup, business needs and preferences. However, let’s briefly discuss some of them and mention possible solutions:

  • dealing with corrupted rows (could be a client’s business choice):
    • logging them and reporting to client after integration is executed
    • failing the whole integration and then reporting
  • clean up if integration failed:
    • removing corrupted S3 files or using abortMultipartUpload
    • sending ACK to the queue but reporting failure or retrying the whole process
  • storing credentials to the client’s storage:
    • specialized service
    • secrets vault
  • sending scheduled integration to a queue:
    • Amazon EventBridge Scheduler
    • Apache Airflow
    • Temporal
    • and many other schedulers.

Where else this pattern applies

As a conclusion I’d like to mention when this pattern can be applied and when it doesn’t make a lot of sense to implement it.

The main benefit of this solution is parallelization of CPU-bound tasks and it can be useful for:

  • Complex business rules — scoring, pricing engines, eligibility checks, fraud detection running across hundreds of thousands of rows
  • Statistical computation — rolling windows, percentile calculations, custom aggregations over large in-memory datasets.
  • Text processing — heavy regex work, tokenization, parsing of custom or proprietary formats.
  • Large JSON processing — JSON.parse and JSON.stringify run synchronously in V8. For small payloads this is negligible, but deserializing a 50MB blob will block noticeably.
  • Templates rendering — generating thousands of HTML emails or reports in a tight loop.

If you suspect your pipeline is doing heavy computations – profile first, and if CPU load is constant and high, consider implementing something similar to the solution above. If you just use Node.js built-in modules like crypto or zlib – they already solved the issue by delegating work to libuv’s C/C++ addons and don’t block the event loop. Always worth checking before implementing.

Even if you don’t find the described implementation useful, I hope you’ve got at least some intuition how to move big files over the network, use Streams, Worker Threads and how some parts of Node.js work.

Stop Drowning in Vectors: How I Built a Graph-Powered RAG That Actually Scales

The Problem with Traditional RAG

Let’s be honest – vector-based RAG has a scaling problem. You chunk documents, embed everything, store it in a vector database, and hope semantic similarity finds the right context. But when you’re dealing with:

  • Hundreds of technical documents
  • Cross-referenced content (citations, related sections)
  • Hierarchical information (chapters → sections → subsections)

Vector search starts to feel like finding a needle in a haystack of needles. You either blow up your context window or miss critical relationships between documents.

Enter Vectorless RAG

The PageIndex architecture introduced a brilliant alternative: parse documents into hierarchical JSON trees and let the LLM navigate the structure directly. No embeddings. No similarity search. Just pure structural reasoning.

But the original approach had a limitation – it kept everything in memory. Try loading hundreds of document trees simultaneously, and you’ll watch your RAM wave goodbye.

Why I Put It on Neo4j

I took the vectorless RAG concept and gave it a persistent backbone: Neo4j Graph Database. Here’s what changed:

1. Persistent Memory at Scale

Instead of loading JSON trees into memory, the hierarchical structure lives in Neo4j. Now you can query millions of documents without breaking a sweat. The LLM starts at document roots and walks down only the branches it needs.

2. Cross-Document Relationships

This is where it gets powerful. Want to link a citation in Document A to its source in Document B? Just create a [:REFERENCES] edge. The LLM can traverse these relationships during retrieval, giving you a true reasoning knowledge graph — something vector search simply cannot replicate.

3. Token-Efficient Retrieval

The LLM acts as an agentic navigator. Given a query, it:

  1. Identifies relevant root sections
  2. Queries Neo4j for children nodes
  3. Drills down iteratively until it finds the answer
  4. Ignores irrelevant branches entirely

This saves massive amounts of context tokens compared to dumping entire documents into the prompt.

How It Works

The Graph Structure

Every document becomes a tree in Neo4j:

(Document)-[:HAS_SECTION]->(Chapter)-[:HAS_SUBSECTION]->(Section)-[:HAS_SUBSECTION]->(Subsection)

The central Document node connects to top-level chapters, which recursively connect to sub-sections. Each node can store summaries, page references, and metadata.

Three-Step Workflow

Step 1: Parse Documents

uv run python main.py --pdf_path /path/to/document.pdf

This generates a _structure.json file containing the hierarchical tree. Markdown is supported too:

uv run python main.py --md_path /path/to/document.md

Step 2: Ingest into Neo4j

uv run python -m src.database.ingest --json_path ./results/document_structure.json

The JSON tree becomes nodes and relationships in your graph database.

Step 3: Query with Agentic Retrieval

uv run python -m src.agent.retriever 
  --doc_name document_structure.json 
  --query "What are the key findings in section 2?"

The LLM identifies relevant sections, traverses the graph, and returns precise answers with page references.

Under the Hood

Tech Stack

  • Neo4j — Graph database for persistent storage
  • LiteLLM — Unified LLM interface (defaults to llama-3.3-70b via Groq)
  • PyMuPDF / PyPDF2 — PDF parsing
  • uv — Lightning-fast Python package management

The Agentic Retrieval Loop

The retriever (src/agent/retriever.py) implements a multi-step reasoning process:

  1. Root Analysis — LLM examines top-level sections to identify candidates
  2. Iterative Drilling — For each candidate, fetch children from Neo4j
  3. Relevance Filtering — LLM decides which branches to explore further
  4. Answer Extraction — Once leaf nodes are reached, extract the answer

This is fundamentally different from vector search. Instead of “find similar chunks,” it’s “navigate to the right place.”

When to Use This Approach

Scenario Vector RAG Graph-Powered Vectorless RAG
Simple Q&A over single doc ✅ ✅
Cross-document reasoning ❌ ✅
Hierarchical content (manuals, specs) ⚠️ ✅
Citation/reference tracking ❌ ✅
Token-efficient retrieval ⚠️ ✅
Massive document collections ❌ ✅

What’s Next

This is just the foundation. Here’s where I’m taking it:

  • Multi-hop reasoning across document collections
  • Dynamic reference extraction to auto-build [:REFERENCES] edges
  • Hybrid search combining graph traversal with optional vector fallback
  • Streaming responses for real-time navigation feedback

Final Thoughts

Vector embeddings aren’t going away, but they’re not the only tool in the box. For structured, hierarchical, or cross-referenced content, graph-powered vectorless RAG gives you:

  • Scalability — millions of documents, zero memory issues
  • Reasoning — traverse relationships, not just similarities
  • Efficiency — precise context retrieval, minimal token waste

Sometimes the best way forward is to go back to structure.

🔗 Project Repository: https://github.com/TejasS1233/vectorless_RAG

💬 Questions or ideas? Drop them in the comments – I’d love to hear how you’re approaching RAG at scale.

If you found this useful, follow me for more deep dives into practical AI architecture. Next up: building multi-hop reasoning across document graphs.

🏦 Bridging Development and QA: My Journey from Code Design to Manual Testing Mastery

How I Learned Testable Design by Breaking My Own ATM System:

** It Started With a “Working” System**

I built an ATM simulator. It worked. At least, that’s what I thought.

From a developer’s perspective, everything looked fine:

  • Log in ✔
  • Check balance ✔
  • Deposit and withdraw ✔
  • Transfer funds ✔

Then I asked a simple question:

How do I know this system actually works correctly?

That question changed everything.

** The Illusion of “It Works”**

At first, I was testing informally:

  • Try a valid input → works
  • Try another valid input → works

I felt confident. But I wasn’t testing. I was confirming my own assumptions.

** The Turning Point: When I Tried to Test Properly**

I decided to approach my own system like a QA engineer.

Not casually, but systematically.

That’s when I realised something uncomfortable:

I didn’t actually have clearly defined expected behaviour.

** From Code to QA: Where Everything Became Real**

I created a separate repository dedicated to manual testing.

Not random testing, but a structured QA workflow:

  • Requirements
  • Acceptance criteria
  • Test plan
  • Test scenarios
  • Test cases
  • Execution results
  • Defects

** Step 1: Writing Requirements Changed Everything**

Before writing a single test case, I had to answer:

What exactly is the system supposed to do?

So I formalised requirements like:

  • Authentication rules
  • Deposit validation
  • Withdrawal constraints

Each requirement included clear acceptance criteria.

** First Major Realisation**

I had written code before defining behaviour. That’s backwards.

** Step 2: Designing Test Cases Exposed Gaps**

When I started writing test cases, something interesting happened.

I began asking questions like:

  • What happens if the user enters text instead of a number?
  • What if they withdraw exactly the balance?
  • What if they transfer to an invalid account?

These were not edge cases anymore. They were missing behaviours in my design.

** The Moment My System Started Breaking**

When I executed my test scenarios, I found real issues.

** Deposit Validation Issues**

  • Non-numeric input was not handled clearly
  • System behaviour was inconsistent

** Transfer Usability Problem**

  • Error messages were too generic
  • The user did not know what went wrong

** Input Handling Problem**

  • The system returned to the menu instead of re-prompting
  • The user flow was broken

These were not coding mistakes. They were design problems.

** Test Execution Made It Real**

** Execution Summary**

From actual execution:

  • ✅ Authentication → Passed
  • ✅ Balance → Passed
  • ❌ Deposit → Failed
  • ❌ Transfer → Failed
  • ❌ Input Handling → Failed

All backed by:

  • Evidence screenshots
  • Execution notes
  • Scenario tracking

** Second Major Realisation**

Without execution tracking, testing is just opinion.

** Step 3: Defects Told Me the Truth**

I documented each defect clearly:

  • Steps to reproduce
  • Expected vs actual behaviour
  • Evidence
  • Impact

I did not use tools like Jira. I did not need them. Because the value was not in the tool, it was in the clarity of thinking.

** The Most Important Lesson: Design Impacts Testing**

At this point, something became very clear.

** Developer vs QA Mindset**

Developer Thinking QA Thinking
It works What can break?
Focus on happy path Focus on edge cases
Code-focused Behaviour-focused

** Critical Insight**

Poor design creates hidden defects. Testing simply reveals them.

** SDLC Reflection: Where This Matters in Real Life**

This experience made me think beyond my project.

** What Happens in Real Projects?**

If you skip proper planning:

  • Requirements are unclear
  • Test cases are inconsistent
  • Bugs are discovered late
  • Fixes become expensive

** Cost of Poor Planning**

  • Developers rework logic
  • Testers rewrite cases
  • Deadlines slip
  • Confidence drops

** Looking Back at My ATM Design**

Now I see my system differently.

Before Testing

This works fine

After Testing

  • Validation logic incomplete
  • Error messaging unclear
  • User flow inconsistent

** The Shift That Changed Everything**

I stopped asking: Does it work?
And started asking: Can it fail safely?

** What This Means Going Forward**

Because I designed the system with separation of concerns:

  • I can now add an API layer
  • I can automate tests using Pytest
  • I can introduce UI automation

This is no longer just a project. It is a foundation for real QA and automation work.

** Final Takeaways**

  • Writing code is not enough
  • Testability is a design decision
  • Requirements drive testing
  • Testing reveals design flaws
  • Real QA is structured, not random

** Closing Thought**

I started this project to build an ATM system, but I ended up learning something much bigger. The moment I stopped treating QA as something you do after coding and started treating it as a design constraint from the beginning, my code improved before I even wrote it. I didn’t just learn how to test software – I learned that the most valuable test cases are the ones that make you realise your design was incomplete

** Repositories**

ATM Simulator

https://github.com/enayetrashid/atm-simulator

Manual Testing Practice

https://github.com/enayetrashid/manual-testing-practice

CodeRabbit for Open Source: AI Code Reviews for OSS Projects

CodeRabbit screenshot

Maintaining an open source project is a time problem. The code itself is usually the easy part. What burns maintainers out is the review queue – a steady stream of pull requests from contributors with varying experience levels, coding styles, and familiarity with your project’s conventions. Every PR needs review. Every review takes time. And the more popular your project gets, the worse the ratio of maintainer hours to incoming contributions becomes.

This is the exact problem that CodeRabbit solves for open source projects – and it does it for free. CodeRabbit Pro features are automatically available on all public repositories with no activation step, no credit card, and no time limit. You install it, point it at your repos, and every pull request gets an AI-powered review within minutes.

This guide covers how CodeRabbit works specifically for open source projects: what the free tier includes, how to configure it for OSS workflows, how it handles external contributor PRs, and practical .coderabbit.yaml examples tuned for the kinds of challenges open source maintainers actually face.

CodeRabbit screenshot

What open source projects get for free

CodeRabbit’s pricing model is simple when it comes to open source: public repositories get Pro-level features for free, forever. There is no trial period and no feature gating based on repository size or contributor count. If your repo is public on GitHub or GitLab, you get the full AI review engine.

Specifically, open source projects receive:

AI-powered PR summaries – Every pull request gets an automated summary that describes what changed, why it likely changed, and how the changes relate to the broader codebase. For maintainers triaging a queue of 20 PRs, these summaries save significant time by letting you prioritize which PRs need your attention first.

Line-by-line review comments – CodeRabbit posts inline comments on the PR diff, pointing out bugs, security issues, style violations, and improvement opportunities. These comments read like a human reviewer’s feedback, with explanations of why something is a problem and suggestions for how to fix it.

40+ built-in linters and SAST tools – On the Pro tier, CodeRabbit integrates with tools like Pylint, ESLint, Ruff, RuboCop, Clippy, Semgrep, Gitleaks, and many more. These run automatically on each PR without requiring the project to configure separate CI jobs for each tool.

Custom review instructions – You can write plain-English instructions in a .coderabbit.yaml file that tell CodeRabbit what to look for. This is how you encode your project’s specific conventions so that every PR – from first-time contributors and long-time maintainers alike – gets reviewed against the same standards.

Sequence diagrams – CodeRabbit generates visual diagrams showing the flow of changes, which helps contributors and reviewers understand the impact of a change at a glance.

Auto-generated docstrings – CodeRabbit can suggest or generate documentation for functions and classes that lack it, which is particularly useful for open source projects where documentation quality directly affects contributor onboarding.

The rate limits on the free tier are reasonable for most open source projects: 200 files per hour and 3 back-to-back reviews followed by 4 reviews per hour per repository. For projects that receive a burst of PRs during a sprint or hackathon, these limits might occasionally throttle reviews, but for steady-state contribution patterns they are rarely an issue.

The main features that public repos do not get are the dashboard and reporting tools – analytics about review trends, team performance metrics, and similar enterprise-oriented features. For open source maintainers, these are rarely the priority.

Why AI code review matters more for open source than for companies

At a company, every developer on the team has been through onboarding. They know the codebase’s conventions, they have access to internal documentation, and they attend standup meetings where architectural decisions are discussed. Code review at a company is primarily about catching mistakes in code written by people who already know the rules.

Open source is different. Contributors range from experienced developers who have been contributing for years to first-time contributors who found your project yesterday. They may not have read your contributing guide. They may not know your naming conventions, your test requirements, or your preferred error handling patterns. And they are often contributing in their spare time, which means they may not be responsive to review feedback for days or weeks.

This asymmetry is what makes AI code review so valuable for open source. CodeRabbit acts as a first-pass reviewer that:

  • Catches the mechanical issues (style, formatting, missing tests, unused imports) so maintainers do not waste their limited review time on them
  • Provides immediate feedback to contributors, regardless of what timezone the maintainers are in
  • Enforces project conventions consistently across every PR, even when different maintainers have slightly different review styles
  • Reduces the friction of contributing by giving contributors feedback within minutes rather than waiting days for a human reviewer

For small open source projects with one or two maintainers, CodeRabbit effectively acts as a third team member who handles the tedious parts of review. For large projects with dozens of weekly PRs, it acts as a triage layer that ensures maintainers only need to focus on the PRs that require genuine human judgment.

Setting up CodeRabbit on a public repository

Getting CodeRabbit running on an open source project takes less than five minutes.

GitHub setup

  1. Go to coderabbit.ai and sign in with your GitHub account
  2. Install the CodeRabbit GitHub App from the GitHub Marketplace
  3. Select the repositories you want CodeRabbit to review – you can grant access to all repos in your organization or select specific ones
  4. Open a pull request on any selected repository – CodeRabbit will automatically post its first review within 2 to 5 minutes

That is it. No YAML configuration is required for the default behavior to be useful. CodeRabbit will summarize the PR, post inline review comments, and generate a walkthrough of the changes.

GitLab setup

The process is similar for GitLab. Sign in at coderabbit.ai with your GitLab account, authorize the integration, select your public projects, and CodeRabbit will review merge requests automatically.

Azure DevOps and Bitbucket

CodeRabbit also supports Azure DevOps and Bitbucket, though the open source ecosystem on these platforms is smaller. The setup process follows the same pattern: authenticate, select repositories, and CodeRabbit begins reviewing.

Configuring .coderabbit.yaml for open source projects

While CodeRabbit works out of the box, adding a .coderabbit.yaml configuration file to your repository root lets you tailor the review to your project’s specific needs. This is where CodeRabbit becomes genuinely powerful for open source maintainers – you can encode your project’s conventions once and have them enforced on every PR automatically.

Here is a complete configuration example designed for a typical open source project:

language: "en-US"
tone_instructions: "Be helpful and encouraging. Explain why changes are suggested, not just what to change."

reviews:
  profile: "chill"
  request_changes_workflow: false
  high_level_summary: true
  poem: false
  review_status: true
  collapse_walkthrough: false
  sequence_diagrams: true

  auto_review:
    enabled: true
    ignore_title_keywords:
      - "WIP"
      - "DO NOT MERGE"
      - "draft"

  path_filters:
    - "!dist/**"
    - "!build/**"
    - "!**/package-lock.json"
    - "!**/yarn.lock"
    - "!**/pnpm-lock.yaml"
    - "!**/*.min.js"
    - "!**/*.min.css"
    - "!**/*.generated.*"
    - "!docs/api/**"

  path_instructions:
    - path: "**/*"
      instructions: |
        This is an open source project. Contributors may be first-time contributors.
        Be constructive and explain the reasoning behind suggestions.
        Flag breaking changes to public APIs.
        Ensure new public functions and classes have documentation.
        Check that error messages are clear and actionable.
    - path: "tests/**"
      instructions: |
        Verify that tests have meaningful assertions.
        Flag tests that test implementation details rather than behavior.
        Suggest descriptive test names that explain the expected behavior.
    - path: "src/**"
      instructions: |
        Check for consistent error handling patterns.
        Flag any hardcoded credentials, API keys, or secrets.
        Ensure exported functions have JSDoc or docstring documentation.

  tools:
    gitleaks:
      enabled: true
    semgrep:
      enabled: true

Let me walk through the design decisions in this configuration.

Tone matters for open source

The tone_instructions line is one of the most important settings for open source projects. Contributors are volunteers. A review that feels harsh or dismissive – even if technically correct – can drive contributors away permanently. Setting the tone to be “helpful and encouraging” while still providing substantive feedback strikes the right balance.

The profile: "chill" setting reinforces this by reducing the volume of nitpick-level comments. For open source projects, focusing on meaningful issues rather than exhaustive style enforcement keeps the signal-to-noise ratio high.

Filtering out noise

The path_filters section is critical for open source repos. Lock files, minified assets, generated documentation, and build artifacts should never be reviewed. Without these filters, CodeRabbit will post comments on auto-generated files that no human wrote, which creates noise that annoys both contributors and maintainers.

Skipping work-in-progress PRs

The ignore_title_keywords setting tells CodeRabbit to skip PRs that contain “WIP”, “DO NOT MERGE”, or “draft” in the title. This is a small but important quality-of-life feature for maintainers who use draft PRs for work in progress – there is no point in reviewing code that the contributor has not finished writing yet.

Path-specific instructions

The path_instructions section is where you encode project-specific conventions. The global instructions (**/*) apply to every file and focus on the concerns that matter most for open source: documentation, breaking API changes, and clear error messages. Test-specific and source-specific instructions add additional rules for those directories.

Security tools enabled by default

For open source projects, enabling Gitleaks and Semgrep is a high-value, low-effort decision. Gitleaks catches accidentally committed secrets – API keys, tokens, passwords – which is a particularly common mistake when external contributors copy example configurations without sanitizing credentials. Semgrep provides lightweight SAST scanning that catches security vulnerabilities without the heavyweight configuration of a full security pipeline.

Handling external contributor PRs

External contributor PRs are the core workflow of open source, and CodeRabbit handles them well. When a contributor opens a PR, CodeRabbit reviews it immediately – the contributor does not need a CodeRabbit account, and they do not need any special permissions.

What the contributor experience looks like

When a contributor opens a PR, they see CodeRabbit’s review appear as comments on their PR within a few minutes. The review includes:

  1. A summary comment at the top of the PR describing what the changes do and how they affect the project
  2. Inline comments on specific lines of the diff, pointing out issues or suggesting improvements
  3. A walkthrough section showing the files changed and how they relate to each other

Contributors can interact with CodeRabbit directly by replying to its comments. If CodeRabbit flags something that the contributor disagrees with, they can explain their reasoning in a reply and CodeRabbit will reconsider. If CodeRabbit’s suggestion is unclear, the contributor can ask for clarification.

This interaction model is valuable because it gives contributors immediate feedback they can act on, rather than waiting for a maintainer to review their PR. Many contributors – especially first-time contributors to a project – are anxious about whether their code is good enough. Getting automated feedback within minutes reduces that anxiety and helps them iterate faster.

Review consistency across contributors

One of the persistent challenges in open source is maintaining consistency when different maintainers have different review styles. One maintainer might care deeply about documentation quality. Another might focus primarily on test coverage. A third might have strong opinions about error handling patterns.

CodeRabbit enforces the same standards on every PR, regardless of who reviews it afterward. If your .coderabbit.yaml says that public functions must have documentation, CodeRabbit will flag undocumented functions consistently – every time, on every PR, without exception. This consistency frees maintainers to focus on the architectural and design aspects of review where human judgment genuinely matters.

Reducing time-to-first-review

For open source contributors, one of the most frustrating experiences is opening a PR and waiting days or weeks for any feedback at all. This is especially common in projects where maintainers have day jobs and can only review in their spare time.

CodeRabbit provides a response within minutes. While it is not a substitute for maintainer review – only a human can evaluate whether a change aligns with the project’s roadmap and architectural direction – it gives contributors something to work with immediately. Contributors can address CodeRabbit’s feedback while waiting for a maintainer, which means that when the maintainer does get to the PR, the mechanical issues have already been resolved and the PR is in much better shape.

Advanced configuration for common OSS patterns

Monorepo configuration

Many open source projects are structured as monorepos with multiple packages or modules. CodeRabbit’s path-based instructions work well for this pattern:

reviews:
  path_instructions:
    - path: "packages/core/**"
      instructions: |
        This is the core library. Changes here affect all downstream packages.
        Flag any breaking changes to exported APIs.
        Require unit tests for all new public functions.
        Enforce strict TypeScript - no use of 'any' type.
    - path: "packages/cli/**"
      instructions: |
        Review CLI commands for consistent help text and error messages.
        Ensure all user-facing output is clear and actionable.
        Check that new commands are documented in the CLI help.
    - path: "packages/plugin-*/**"
      instructions: |
        Plugins should follow the plugin API contract.
        Flag direct imports from core internals - use the public API.
        Ensure backward compatibility with the documented plugin interface.

This structure lets you apply different review standards to different parts of your monorepo, which reflects the reality that core library code, CLI code, and plugin code have different quality requirements.

Documentation-focused configuration

For projects where documentation quality is as important as code quality – reference implementations, educational projects, framework documentation sites – you can configure CodeRabbit to pay special attention to docs:

reviews:
  path_instructions:
    - path: "docs/**"
      instructions: |
        Check that code examples in documentation actually compile and run.
        Flag broken internal links.
        Ensure new features referenced in docs have corresponding API documentation.
        Check that examples follow the project's recommended patterns.
    - path: "examples/**"
      instructions: |
        Examples must be self-contained and runnable.
        Check that examples use current API patterns, not deprecated ones.
        Ensure examples include error handling to demonstrate best practices.

Branch protection and review requirements

For open source projects that use branch protection rules requiring a certain number of approving reviews, CodeRabbit can be configured to work alongside these rules. By default, CodeRabbit does not approve or request changes – it only comments. This means it does not count toward required review approvals, which is the correct behavior for most projects. You want a human to approve, with CodeRabbit providing supporting analysis.

If you want CodeRabbit to approve PRs that pass all checks (which is more appropriate for internal projects than open source), you can enable request_changes_workflow: true in your configuration. For open source, leaving this as false is almost always the right choice.

How popular open source projects use CodeRabbit

CodeRabbit is used by several well-known open source projects, which validates its usefulness for real-world OSS workflows.

Appsmith – the low-code platform for building internal tools – uses CodeRabbit to review contributions across their large codebase. Neon – the serverless Postgres platform – relies on it for reviewing database-related changes where correctness is critical. Plane – the open source project management tool – and Novu – the open source notification infrastructure – both use CodeRabbit to handle their active contributor communities.

Other notable open source adopters include Documenso (open source DocuSign alternative), Formbricks (survey and experience management), NextUI (React UI library), Permify (authorization engine), and Pipedream (API integration platform). These projects span different languages, frameworks, and problem domains, which speaks to CodeRabbit’s language-agnostic review capabilities.

What these projects have in common is high contributor volume – they all receive enough external contributions that manual review is a significant maintainer burden. CodeRabbit scales with contribution volume in a way that human review capacity cannot.

CodeRabbit’s open source sponsorship program

Beyond providing free Pro features for public repositories, CodeRabbit has made a broader commitment to the open source ecosystem. In 2025, the company distributed over $600,000 in sponsorships to open source maintainers, and has since scaled that commitment to $1 million as the company raised its Series B funding.

This matters because it signals long-term commitment to the open source community rather than a marketing-driven free tier that could be pulled at any time. For maintainers considering whether to invest time configuring CodeRabbit for their project, knowing that the company has a genuine financial stake in the open source ecosystem provides some assurance that the free tier will persist.

Comparing CodeRabbit’s OSS offering to alternatives

Manual review (the status quo)

Most open source projects still rely entirely on manual review by maintainers. This works for small projects with a handful of contributors, but it does not scale. The typical failure mode is a growing backlog of unreviewed PRs, which discourages contributors and slows the project’s development velocity.

CodeRabbit does not eliminate the need for maintainer review – it reduces the scope of what maintainers need to review. When CodeRabbit has already caught the style issues, the security vulnerabilities, and the missing documentation, the maintainer can focus on whether the change is architecturally sound and aligned with the project’s direction. That is a much more efficient use of limited maintainer time.

GitHub Actions-based linting

Many open source projects run linters and formatters through GitHub Actions – ESLint, Prettier, Pylint, Ruff, and similar tools. This catches deterministic rule violations but provides no contextual analysis. A linter will tell you that a line is too long. It will not tell you that a function is doing too much and should be split into smaller units with clearer responsibilities.

CodeRabbit complements rather than replaces CI linting. The recommended setup is deterministic linters in CI for hard enforcement (fail the build on violations) and CodeRabbit for contextual AI analysis that catches the issues linters miss.

CodeAnt AI

CodeAnt AI is an alternative worth knowing about, priced at $24-40/user/month. It bundles AI code review with SAST, secret detection, infrastructure-as-code security scanning, and DORA metrics in a single platform. However, CodeAnt AI does not have a free tier, which makes it a non-starter for most open source projects that need zero-cost tooling.

For open source projects that also need security compliance tooling – for example, projects maintained by companies that need SOC 2 or similar certifications – CodeAnt AI can consolidate multiple paid tools into one subscription. But for pure open source community projects, CodeRabbit’s free Pro tier is the clear choice.

Other AI review tools

Several other AI code review tools exist in the market, but few match CodeRabbit’s open source offering. Most either do not have a free tier at all, have free tiers limited to private repos only, or restrict AI features to paid plans. CodeRabbit is unusual in offering the full Pro feature set for free on public repositories.

Tips for getting the most out of CodeRabbit on OSS repos

Write a clear CONTRIBUTING.md and reference it

CodeRabbit’s review quality improves when your project has a clear CONTRIBUTING.md file. Not because CodeRabbit reads the file directly (it reads the .coderabbit.yaml for instructions), but because the two documents should be aligned. Your CONTRIBUTING.md tells humans what you expect. Your .coderabbit.yaml tells CodeRabbit what to enforce. When these are consistent, contributors get the same message from both the documentation and the automated review.

Start with chill profile, tighten later

If your project has never had automated code review, jumping straight to profile: "assertive" will overwhelm contributors with comments on every PR. Start with profile: "chill" to catch the most significant issues, and tighten the configuration over time as your project’s contributors become accustomed to automated review feedback.

Use path_filters aggressively

Open source projects accumulate files that should never be reviewed: lock files, generated code, vendor directories, migration files, changelog entries. Every file that CodeRabbit reviews unnecessarily is a comment that creates noise. Be aggressive with path_filters to exclude anything that is not hand-written source code.

Enable security tools from day one

For open source projects, secret leaks in PRs are a genuine risk. Contributors who copy configuration examples or test credentials from their local environment may accidentally include real API keys or tokens. Enable Gitleaks in your .coderabbit.yaml from the start – catching one leaked credential justifies the entire setup.

Interact with CodeRabbit in PR comments

CodeRabbit is not a static analysis tool that posts comments and disappears. You can reply to its comments, ask it to explain its reasoning, or tell it that a particular suggestion does not apply. Over time, CodeRabbit learns from these interactions (through its knowledge base feature) and adjusts its review behavior for your repository. Encourage your maintainers and regular contributors to engage with CodeRabbit’s comments rather than ignoring them.

Add project-specific instructions for your domain

Generic code review catches generic issues. If your project has domain-specific conventions – for example, a database project that requires all queries to be parameterized, or a web framework that requires all routes to have authentication middleware – encode those conventions in path_instructions. The more specific your instructions are, the more valuable CodeRabbit’s review becomes.

Getting started

If you maintain an open source project and want to try CodeRabbit:

  1. Go to coderabbit.ai and sign in with your GitHub or GitLab account
  2. Install the CodeRabbit app and select your public repositories
  3. Open a pull request – CodeRabbit reviews it automatically within 2 to 5 minutes
  4. Review CodeRabbit’s feedback and interact with its comments to calibrate its review style
  5. Add a .coderabbit.yaml file with the configuration examples above as a starting point
  6. Customize the configuration over time based on what your project actually needs

The free tier gives you the full Pro review experience on public repos with no time limit. For most open source projects, this is enough to significantly reduce maintainer review burden and improve contribution quality without spending a dollar.

For a deeper look at CodeRabbit’s full feature set and pricing across all tiers, the CodeRabbit review covers everything in detail. And if you want to understand how CodeRabbit works for specific languages, the CodeRabbit for Python guide is a good example of language-specific configuration.

Further Reading

Frequently Asked Questions

Is CodeRabbit free for open source projects?

Yes. CodeRabbit Pro features are automatically available for all public repositories on GitHub and GitLab where CodeRabbit is installed. There is no activation step – you install the CodeRabbit app, point it at your public repos, and Pro-level AI code reviews run on every pull request for free. Rate limits apply at 200 files per hour and 3 back-to-back reviews followed by 4 reviews per hour.

How do I set up CodeRabbit on an open source GitHub repo?

Go to coderabbit.ai and sign in with your GitHub account. Install the CodeRabbit GitHub App and select the public repositories you want reviewed. Open a pull request and CodeRabbit will post its first review automatically within 2 to 5 minutes. Optionally add a .coderabbit.yaml file to your repository root to customize review behavior, path filters, and contributor-facing instructions.

Does CodeRabbit work with external contributor pull requests?

Yes. CodeRabbit reviews every pull request opened against your repository, regardless of whether the author is a maintainer or an external contributor. This is particularly valuable for open source projects because it provides consistent review quality across all contributions without requiring maintainer time for initial review passes.

What features do open source projects get with CodeRabbit free?

Public repositories on GitHub and GitLab get Pro-level features for free including AI-powered PR summaries, line-by-line review comments, sequence diagrams, 40+ built-in linters and SAST tools, custom review instructions via .coderabbit.yaml, and auto-generated docstrings. The main limitations compared to paid plans are rate limits and the absence of dashboard and reporting features.

Can I customize CodeRabbit review instructions for my open source project?

Yes. Add a .coderabbit.yaml file to your repository root with custom instructions in plain English. You can set path-specific review rules, exclude generated files from review, configure the review tone, enable or disable specific linters, and write project-specific conventions that CodeRabbit will enforce on every pull request. The configuration file is version-controlled alongside your code.

How does CodeRabbit compare to manual code review for open source maintainers?

CodeRabbit does not replace maintainer review but significantly reduces the burden. It catches style violations, security issues, common bugs, and missing documentation before a maintainer looks at a PR. For active open source projects receiving dozens of contributions per week, this means maintainers can focus on architectural decisions and design review rather than spending time on mechanical checks that an AI handles reliably.

Originally published at aicodereview.cc

How AI Tools talk to Each Other

Field Name mismatch

For a more interactive version of this post, visit https://bekahhw.com/how-ai-tools-communicate

This weekend, my daughter ran in her first high school track meet. One of the other girls relay teams was disqualified for dropping the baton. I don’t know much about track, so I was surprised to learn that dropping the baton can result in a DQ (disqualification). The thing that really sucks is that those girls were the fastest team, even after having to recover the dropped baton. But, at the end of the meet, it doesn’t matter how fast each runner is if the baton doesn’t make it across the finish line without the team getting DQed. The team has to work together, and the baton is the thing that connects them.

It’s kind of like what’s happening when AI tools communicate. The intelligence of each individual tool matters less than whether they can pass information to each other cleanly. And most beginners don’t realize this until something breaks and they’re staring at an error message with no idea where to start.

Most AI tool communication happens through a small number of patterns. Once you recognize them, debugging stops feeling like magic and starts feeling like plumbing.

Everything is a Message

If you’ve ever wondered why some AI tools feel instant while others make you wait, or why a multi-step AI workflow sometimes just… stops mid-chain, it comes down to three fundamental communication patterns.

When one piece of an AI system needs to talk to another, it sends a message. That message is almost always structured as JSON, which sounds intimidating but is really just organized text.

Think about ordering food at a restaurant. You don’t just say “I want stuff.” You say “I want a burger, medium, no onions, with fries.” That structure is what lets the kitchen actually process your order. JSON is the same idea. It organizes information into labeled fields so the receiving tool knows exactly what it’s looking at.

A simple JSON message might look like this:

{
  "action": "search",
  "query": "best pizza in New York",
  "results": []
}

The API, or Application Programming Interface, is the agreement between two tools about what fields to expect and what format they’ll be in.

Here’s what that looks like in practice. Say you’re building a workflow where someone submits a form on your site, and you want an AI to draft a personalized response. Your form tool sends a message to the LLM that might look like this:

{
  "name": "Jordan",
  "question": "How do I get started with open source?",
  "experience_level": "beginner"
}

The LLM knows to look for those fields because your API agreement says they’ll be there. It uses name to personalize the reply, question to know what to answer, and experience_level to calibrate how technical to get.

Now imagine your form tool sends this instead:

{
  "username": "Jordan",
  "inquiry": "How do I get started with open source?",
  "level": "beginner"
}

Field Name mismatch

The LLM is now confused because it was expecting “name,” “question,” and “experience_level.” The LLM goes looking for name and finds nothing. It goes looking for question and finds nothing. The chain breaks, not because anything was wrong with the content, but because the tools weren’t speaking the same language.

Field Name Fix

When something breaks in a tool chain, it’s almost always because one tool sent a message the next tool didn’t understand. Wrong format. Missing field. Unexpected data type. The fix is rarely complicated. But you have to know that’s where to look.

Three Ways AI Tools Communicate

3 patterns diagram

Request/Response

One tool asks, the other answers. You send a prompt, you get text back, you pass it to the next step. Think of it like sending a text message and waiting for a reply before doing anything else.

Streaming

Instead of waiting for the full response, the output arrives piece by piece. This is why ChatGPT seems to type its answer in real time rather than making you wait for the whole thing to appear at once. It’s useful when you’re generating long content or building something that needs to feel responsive.

Events

Instead of asking and waiting, a tool watches for something to happen and then reacts. A new email arrives. A file is uploaded. A timer fires. The agent picks it up and acts without anyone pressing a button. This is how you build things that run in the background autonomously.

Most builders start with request/response and eventually add streaming when their interface feels sluggish, or events when they want something to run without manual triggering. But the real magic happens when you combine them. You can have a tool chain that starts with an event trigger, streams output to the user, and then sends a final request/response message to update a database.

What Actually Breaks Multi-Step Chains

Each of those three patterns works fine in isolation. Tool chains fail in very predictable ways. If you know the patterns, you know where to look. The problem shows up when you chain tools together and the context window (the AI’s working memory) fills up.

Diagnosing broken chain

Context window overflow.

Every LLM can only “see” a certain amount of text at once. Imagine trying to read a book but you can only ever see 10 pages at a time. If you keep shoving earlier chapters into the window to maintain “memory,” you eventually run out of room for the chapter you’re actually trying to read. Builders who chain multiple tools together can accidentally fill the context window with outputs from earlier steps, leaving no room for the actual task. Smart builders decide what to pass forward and what to leave behind.

Malformed outputs.

If step three in your chain expects an organized JSON object and step two returns a casual paragraph of text, step three breaks. It’s like asking someone to fill out a form, but instead of using the form fields, they just write you a letter. The information might be there, but the system can’t process it. This is why explicitly telling the LLM how to format its output, something like “respond only in JSON with these exact fields,” matters more than most people expect.

Latency compounding.

Each step takes time. Three tools that each take two seconds is at minimum six seconds total, plus overhead. If you’re building something people interact with in real time, that adds up fast. Builders solve this with caching, which means storing results you’ve already computed so you don’t recalculate them, and parallelism, which means running independent steps at the same time instead of one after another.

Vague instructions at the orchestration level.

The LLM decides which tool to call next based on the instructions you’ve given it. Vague instructions lead to the wrong tool getting called, or the right tool getting called with the wrong inputs. Think of it like giving someone directions. “Head toward the big building” leaves too much room for interpretation. “Turn left at the red light, go two blocks, turn right at the gas station” gets you where you need to go. The precision of your orchestration prompt determines whether your agent behaves reliably or keeps guessing.

The mental shift that changes how you AI

When you start thinking in tool chains, you stop asking “what can I get the AI to do?” and start asking “what does each step need to receive, and what does it need to output?”

That’s a systems question. And it’s actually a more useful frame than prompt craft alone, because it forces you to get specific about your requirements before you write a single instruction.