{"@attributes":{"version":"2.0"},"channel":{"title":"DEV Community","description":"The most recent home feed on DEV Community.","link":"https:\/\/dev.to","language":"en","item":[{"title":"I built a state management library for Rust called GLoC","pubDate":"Sun, 07 Jun 2026 19:11:58 +0000","link":"https:\/\/dev.to\/godwin_jk\/i-built-a-state-management-library-for-rust-called-gloc-3o1h","guid":"https:\/\/dev.to\/godwin_jk\/i-built-a-state-management-library-for-rust-called-gloc-3o1h","description":"<p>Hey, <br>\nI built a state management library for Rust called <strong>GLoC<\/strong>(Global Logic Component) \u2014 it's basically Bloc from Flutter but for Rust.<\/p>\n\n<p>Just published it on crates.io, would love some feedback.<\/p>\n\n<p><a href=\"https:\/\/crates.io\/search?q=gloc\" rel=\"noopener noreferrer\">Crate<\/a><br>\n<a href=\"https:\/\/github.com\/godwinjk\/gloc\" rel=\"noopener noreferrer\">Repo<\/a><\/p>\n\n<p>If you're not familiar with Bloc, I have a doc in the repo explaining it. Also wrote a few Medium articles walking through it with Dioxus \u2014 links in the repo.<\/p>\n\n<p><a href=\"https:\/\/medium.com\/@godwinjoseph.k\/gloc-reactor-the-missing-state-management-pattern-for-rust-6140ccefb79c\" rel=\"noopener noreferrer\">Medium tutorial<\/a><\/p>\n\n<p>Still early, any thoughts welcome!<\/p>\n\n","category":["rust","gloc","bloc"]},{"title":"SQLite in Production: Why We Chose It Over Postgres for a Multi-Tenant SaaS","pubDate":"Sun, 07 Jun 2026 19:11:31 +0000","link":"https:\/\/dev.to\/helperx\/sqlite-in-production-why-we-chose-it-over-postgres-for-a-multi-tenant-saas-44n8","guid":"https:\/\/dev.to\/helperx\/sqlite-in-production-why-we-chose-it-over-postgres-for-a-multi-tenant-saas-44n8","description":"<p>\"You're using SQLite in production? For a SaaS?\" \u2014 every developer who hears about our stack.<\/p>\n\n<p>Yes. And after a year of running it with hundreds of active accounts, I'd make the same choice again. Here's why \u2014 and the specific patterns that make it work for multi-tenant workloads.<\/p>\n\n<h2>\n  \n  \n  The architecture decision\n<\/h2>\n\n<p>HelperX manages multiple X accounts. Each account is an isolated \"slot\" with its own configuration, auth tokens, audit logs, and operational state. When we designed the data layer, we had two options:<\/p>\n\n<p><strong>Option A: Shared Postgres<\/strong><br>\nOne PostgreSQL database. All slots share tables. Isolation enforced by <code>WHERE slot_id = ?<\/code> on every query.<\/p>\n\n<p><strong>Option B: SQLite per slot<\/strong><br>\nOne SQLite database file per slot. Complete physical isolation. No shared state.<\/p>\n\n<p>We chose Option B. Here's the decision matrix:<\/p>\n\n<div class=\"table-wrapper-paragraph\"><table>\n<thead>\n<tr>\n<th>Factor<\/th>\n<th>Shared Postgres<\/th>\n<th>SQLite per slot<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Data isolation<\/td>\n<td>Logical (WHERE clause)<\/td>\n<td>Physical (separate files)<\/td>\n<\/tr>\n<tr>\n<td>Deployment complexity<\/td>\n<td>Requires Postgres server<\/td>\n<td>Zero \u2014 embedded<\/td>\n<\/tr>\n<tr>\n<td>Backup granularity<\/td>\n<td>Full DB or table-level<\/td>\n<td>Per-slot file copy<\/td>\n<\/tr>\n<tr>\n<td>Slot deletion<\/td>\n<td>DELETE + potential orphans<\/td>\n<td><code>rm slot_abc.db<\/code><\/td>\n<\/tr>\n<tr>\n<td>Concurrent writes<\/td>\n<td>Excellent<\/td>\n<td>Limited (WAL mode helps)<\/td>\n<\/tr>\n<tr>\n<td>Read performance<\/td>\n<td>Network round-trip<\/td>\n<td>Local disk, sub-ms<\/td>\n<\/tr>\n<tr>\n<td>Operational overhead<\/td>\n<td>Monitoring, upgrades, HA<\/td>\n<td>None<\/td>\n<\/tr>\n<tr>\n<td>Cost<\/td>\n<td>$20-100\/mo (managed DB)<\/td>\n<td>$0<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/div>\n\n<p>For our workload \u2014 lots of reads, moderate writes, strong isolation requirements \u2014 SQLite won on every axis except concurrent writes. And our write pattern (one slot writes sequentially, never concurrently) means that limitation doesn't apply.<\/p>\n<h2>\n  \n  \n  The implementation\n<\/h2>\n<h3>\n  \n  \n  Database-per-slot pattern\n<\/h3>\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight javascript\"><code><span class=\"k\">import<\/span> <span class=\"nx\">Database<\/span> <span class=\"k\">from<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">better-sqlite3<\/span><span class=\"dl\">'<\/span><span class=\"p\">;<\/span>\n<span class=\"k\">import<\/span> <span class=\"nx\">path<\/span> <span class=\"k\">from<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">path<\/span><span class=\"dl\">'<\/span><span class=\"p\">;<\/span>\n<span class=\"k\">import<\/span> <span class=\"nx\">fs<\/span> <span class=\"k\">from<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">fs<\/span><span class=\"dl\">'<\/span><span class=\"p\">;<\/span>\n\n<span class=\"kd\">const<\/span> <span class=\"nx\">DB_DIR<\/span> <span class=\"o\">=<\/span> <span class=\"nx\">path<\/span><span class=\"p\">.<\/span><span class=\"nf\">join<\/span><span class=\"p\">(<\/span><span class=\"nx\">process<\/span><span class=\"p\">.<\/span><span class=\"nf\">cwd<\/span><span class=\"p\">(),<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">data<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">slots<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n\n<span class=\"kd\">function<\/span> <span class=\"nf\">getSlotDb<\/span><span class=\"p\">(<\/span><span class=\"nx\">slotId<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n  <span class=\"kd\">const<\/span> <span class=\"nx\">dbPath<\/span> <span class=\"o\">=<\/span> <span class=\"nx\">path<\/span><span class=\"p\">.<\/span><span class=\"nf\">join<\/span><span class=\"p\">(<\/span><span class=\"nx\">DB_DIR<\/span><span class=\"p\">,<\/span> <span class=\"s2\">`<\/span><span class=\"p\">${<\/span><span class=\"nx\">slotId<\/span><span class=\"p\">}<\/span><span class=\"s2\">.db`<\/span><span class=\"p\">);<\/span>\n  <span class=\"kd\">const<\/span> <span class=\"nx\">db<\/span> <span class=\"o\">=<\/span> <span class=\"k\">new<\/span> <span class=\"nc\">Database<\/span><span class=\"p\">(<\/span><span class=\"nx\">dbPath<\/span><span class=\"p\">);<\/span>\n\n  <span class=\"nx\">db<\/span><span class=\"p\">.<\/span><span class=\"nf\">pragma<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">journal_mode = WAL<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n  <span class=\"nx\">db<\/span><span class=\"p\">.<\/span><span class=\"nf\">pragma<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">synchronous = NORMAL<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n  <span class=\"nx\">db<\/span><span class=\"p\">.<\/span><span class=\"nf\">pragma<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">foreign_keys = ON<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n  <span class=\"nx\">db<\/span><span class=\"p\">.<\/span><span class=\"nf\">pragma<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">busy_timeout = 5000<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n\n  <span class=\"k\">return<\/span> <span class=\"nx\">db<\/span><span class=\"p\">;<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>Each slot gets its own <code>.db<\/code> file. The pragmas are critical:<\/p>\n\n<ul>\n<li>\n<strong>WAL mode:<\/strong> Allows concurrent reads while writing. Without this, readers block on writes.<\/li>\n<li>\n<strong>synchronous = NORMAL:<\/strong> Trades a small durability risk for 5-10x write speed. For our workload (losing the last 1-2 log entries in a crash is acceptable), this is the right tradeoff.<\/li>\n<li>\n<strong>busy_timeout:<\/strong> If a write is in progress, wait up to 5 seconds instead of throwing immediately. Prevents spurious <code>SQLITE_BUSY<\/code> errors.<\/li>\n<\/ul>\n<h3>\n  \n  \n  Schema per slot\n<\/h3>\n\n<p>Every slot database has identical schema:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight javascript\"><code><span class=\"kd\">function<\/span> <span class=\"nf\">initSlotSchema<\/span><span class=\"p\">(<\/span><span class=\"nx\">db<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n  <span class=\"nx\">db<\/span><span class=\"p\">.<\/span><span class=\"nf\">exec<\/span><span class=\"p\">(<\/span><span class=\"s2\">`\n    CREATE TABLE IF NOT EXISTS config (\n      key TEXT PRIMARY KEY,\n      value TEXT NOT NULL,\n      updated_at TEXT DEFAULT (datetime('now'))\n    );\n\n    CREATE TABLE IF NOT EXISTS audit_log (\n      id TEXT PRIMARY KEY,\n      module TEXT NOT NULL,\n      action TEXT NOT NULL,\n      target TEXT,\n      detail TEXT,\n      status TEXT NOT NULL,\n      duration_ms INTEGER,\n      timestamp TEXT NOT NULL DEFAULT (datetime('now')),\n      metadata TEXT\n    );\n\n    CREATE TABLE IF NOT EXISTS queue (\n      id INTEGER PRIMARY KEY AUTOINCREMENT,\n      module TEXT NOT NULL,\n      payload TEXT NOT NULL,\n      status TEXT DEFAULT 'pending',\n      created_at TEXT DEFAULT (datetime('now')),\n      processed_at TEXT\n    );\n\n    CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit_log(timestamp DESC);\n    CREATE INDEX IF NOT EXISTS idx_queue_status ON queue(status, created_at);\n  `<\/span><span class=\"p\">);<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Three tables cover everything a slot needs. Config stores module settings (encrypted). Audit log stores action history. Queue stores pending actions.<\/p>\n\n<h3>\n  \n  \n  Connection pooling (you don't need it)\n<\/h3>\n\n<p>With Postgres, connection pooling is essential. With SQLite, it's unnecessary \u2014 each database connection is a file handle, not a network socket. Opening a SQLite database takes microseconds.<\/p>\n\n<p>We do cache connections to avoid repeated file opens:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight javascript\"><code><span class=\"kd\">const<\/span> <span class=\"nx\">connections<\/span> <span class=\"o\">=<\/span> <span class=\"k\">new<\/span> <span class=\"nc\">Map<\/span><span class=\"p\">();<\/span>\n\n<span class=\"kd\">function<\/span> <span class=\"nf\">getDb<\/span><span class=\"p\">(<\/span><span class=\"nx\">slotId<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n  <span class=\"k\">if <\/span><span class=\"p\">(<\/span><span class=\"nx\">connections<\/span><span class=\"p\">.<\/span><span class=\"nf\">has<\/span><span class=\"p\">(<\/span><span class=\"nx\">slotId<\/span><span class=\"p\">))<\/span> <span class=\"p\">{<\/span>\n    <span class=\"k\">return<\/span> <span class=\"nx\">connections<\/span><span class=\"p\">.<\/span><span class=\"nf\">get<\/span><span class=\"p\">(<\/span><span class=\"nx\">slotId<\/span><span class=\"p\">);<\/span>\n  <span class=\"p\">}<\/span>\n\n  <span class=\"kd\">const<\/span> <span class=\"nx\">db<\/span> <span class=\"o\">=<\/span> <span class=\"nf\">getSlotDb<\/span><span class=\"p\">(<\/span><span class=\"nx\">slotId<\/span><span class=\"p\">);<\/span>\n  <span class=\"nf\">initSlotSchema<\/span><span class=\"p\">(<\/span><span class=\"nx\">db<\/span><span class=\"p\">);<\/span>\n  <span class=\"nx\">connections<\/span><span class=\"p\">.<\/span><span class=\"nf\">set<\/span><span class=\"p\">(<\/span><span class=\"nx\">slotId<\/span><span class=\"p\">,<\/span> <span class=\"nx\">db<\/span><span class=\"p\">);<\/span>\n  <span class=\"k\">return<\/span> <span class=\"nx\">db<\/span><span class=\"p\">;<\/span>\n<span class=\"p\">}<\/span>\n\n<span class=\"kd\">function<\/span> <span class=\"nf\">closeDb<\/span><span class=\"p\">(<\/span><span class=\"nx\">slotId<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n  <span class=\"kd\">const<\/span> <span class=\"nx\">db<\/span> <span class=\"o\">=<\/span> <span class=\"nx\">connections<\/span><span class=\"p\">.<\/span><span class=\"nf\">get<\/span><span class=\"p\">(<\/span><span class=\"nx\">slotId<\/span><span class=\"p\">);<\/span>\n  <span class=\"k\">if <\/span><span class=\"p\">(<\/span><span class=\"nx\">db<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n    <span class=\"nx\">db<\/span><span class=\"p\">.<\/span><span class=\"nf\">close<\/span><span class=\"p\">();<\/span>\n    <span class=\"nx\">connections<\/span><span class=\"p\">.<\/span><span class=\"k\">delete<\/span><span class=\"p\">(<\/span><span class=\"nx\">slotId<\/span><span class=\"p\">);<\/span>\n  <span class=\"p\">}<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Simple <code>Map<\/code> caching. No pool manager, no connection limits, no timeout configuration.<\/p>\n\n<h2>\n  \n  \n  What about migrations?\n<\/h2>\n\n<p>Schema migrations on 200+ SQLite databases sound nightmarish. In practice, it's straightforward:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight javascript\"><code><span class=\"kd\">const<\/span> <span class=\"nx\">MIGRATIONS<\/span> <span class=\"o\">=<\/span> <span class=\"p\">[<\/span>\n  <span class=\"p\">{<\/span>\n    <span class=\"na\">version<\/span><span class=\"p\">:<\/span> <span class=\"mi\">1<\/span><span class=\"p\">,<\/span>\n    <span class=\"na\">up<\/span><span class=\"p\">:<\/span> <span class=\"s2\">`ALTER TABLE audit_log ADD COLUMN metadata TEXT;`<\/span>\n  <span class=\"p\">},<\/span>\n  <span class=\"p\">{<\/span>\n    <span class=\"na\">version<\/span><span class=\"p\">:<\/span> <span class=\"mi\">2<\/span><span class=\"p\">,<\/span>\n    <span class=\"na\">up<\/span><span class=\"p\">:<\/span> <span class=\"s2\">`CREATE INDEX IF NOT EXISTS idx_audit_module ON audit_log(module, status);`<\/span>\n  <span class=\"p\">},<\/span>\n<span class=\"p\">];<\/span>\n\n<span class=\"kd\">function<\/span> <span class=\"nf\">migrate<\/span><span class=\"p\">(<\/span><span class=\"nx\">db<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n  <span class=\"nx\">db<\/span><span class=\"p\">.<\/span><span class=\"nf\">exec<\/span><span class=\"p\">(<\/span><span class=\"s2\">`\n    CREATE TABLE IF NOT EXISTS _migrations (\n      version INTEGER PRIMARY KEY,\n      applied_at TEXT DEFAULT (datetime('now'))\n    )\n  `<\/span><span class=\"p\">);<\/span>\n\n  <span class=\"kd\">const<\/span> <span class=\"nx\">applied<\/span> <span class=\"o\">=<\/span> <span class=\"nx\">db<\/span><span class=\"p\">.<\/span><span class=\"nf\">prepare<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">SELECT MAX(version) as v FROM _migrations<\/span><span class=\"dl\">'<\/span><span class=\"p\">).<\/span><span class=\"nf\">get<\/span><span class=\"p\">();<\/span>\n  <span class=\"kd\">const<\/span> <span class=\"nx\">currentVersion<\/span> <span class=\"o\">=<\/span> <span class=\"nx\">applied<\/span><span class=\"p\">?.<\/span><span class=\"nx\">v<\/span> <span class=\"o\">||<\/span> <span class=\"mi\">0<\/span><span class=\"p\">;<\/span>\n\n  <span class=\"k\">for <\/span><span class=\"p\">(<\/span><span class=\"kd\">const<\/span> <span class=\"nx\">migration<\/span> <span class=\"k\">of<\/span> <span class=\"nx\">MIGRATIONS<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n    <span class=\"k\">if <\/span><span class=\"p\">(<\/span><span class=\"nx\">migration<\/span><span class=\"p\">.<\/span><span class=\"nx\">version<\/span> <span class=\"o\">&gt;<\/span> <span class=\"nx\">currentVersion<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n      <span class=\"nx\">db<\/span><span class=\"p\">.<\/span><span class=\"nf\">exec<\/span><span class=\"p\">(<\/span><span class=\"nx\">migration<\/span><span class=\"p\">.<\/span><span class=\"nx\">up<\/span><span class=\"p\">);<\/span>\n      <span class=\"nx\">db<\/span><span class=\"p\">.<\/span><span class=\"nf\">prepare<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">INSERT INTO _migrations (version) VALUES (?)<\/span><span class=\"dl\">'<\/span><span class=\"p\">).<\/span><span class=\"nf\">run<\/span><span class=\"p\">(<\/span><span class=\"nx\">migration<\/span><span class=\"p\">.<\/span><span class=\"nx\">version<\/span><span class=\"p\">);<\/span>\n    <span class=\"p\">}<\/span>\n  <span class=\"p\">}<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Migrations run on database open. Each slot migrates independently. If a migration fails on one slot, it doesn't affect others. Compare this to a shared Postgres migration that locks the entire database.<\/p>\n\n<h2>\n  \n  \n  The global database\n<\/h2>\n\n<p>Not everything belongs in per-slot databases. Platform-level data \u2014 user accounts, billing, plan configuration \u2014 lives in a single global SQLite database:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>data\/\n\u251c\u2500\u2500 global.db          \u2190 users, plans, billing\n\u2514\u2500\u2500 slots\/\n    \u251c\u2500\u2500 slot_abc.db    \u2190 slot A config + logs\n    \u251c\u2500\u2500 slot_def.db    \u2190 slot B config + logs\n    \u2514\u2500\u2500 slot_ghi.db    \u2190 slot C config + logs\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>The global database handles authentication and authorization. Slot databases handle operations. No cross-database joins needed \u2014 the application layer bridges them when necessary.<\/p>\n\n<h2>\n  \n  \n  Performance reality\n<\/h2>\n\n<p>After a year of production data:<\/p>\n\n<p><strong>Read performance:<\/strong><\/p>\n\n<ul>\n<li>Config lookup: 0.02ms (vs 2-5ms with Postgres over network)<\/li>\n<li>Audit log query (last 24h): 0.3ms<\/li>\n<li>Dashboard aggregation: 1.2ms<\/li>\n<\/ul>\n\n<p><strong>Write performance:<\/strong><\/p>\n\n<ul>\n<li>Single audit log insert: 0.05ms<\/li>\n<li>Batch insert (100 rows): 2.1ms<\/li>\n<li>Config update: 0.04ms<\/li>\n<\/ul>\n\n<p><strong>Storage:<\/strong><\/p>\n\n<ul>\n<li>Average slot database: 2.4MB after 90 days<\/li>\n<li>Global database: 1.8MB<\/li>\n<li>Total for 200 slots: ~480MB<\/li>\n<\/ul>\n\n<p>For comparison, a managed Postgres instance with equivalent data would cost $20-50\/month and add 2-5ms latency to every query. Our SQLite setup costs $0 and responds in microseconds.<\/p>\n\n<h2>\n  \n  \n  The tradeoffs (honestly)\n<\/h2>\n\n<p>SQLite isn't perfect. Here's what we gave up:<\/p>\n\n<p><strong>1. No concurrent writes across connections.<\/strong><br>\nIf two processes try to write to the same slot database simultaneously, one blocks. For our workload (one worker per slot, sequential operations), this isn't an issue. For a web app with 50 concurrent users editing the same record, it would be.<\/p>\n\n<p><strong>2. No replication.<\/strong><br>\nSQLite doesn't replicate. Our backup strategy is filesystem-level: daily snapshots of the <code>data\/<\/code> directory. If we needed real-time replication, we'd need Litestream or a similar tool.<\/p>\n\n<p><strong>3. No network access.<\/strong><br>\nSQLite is local. If we split into microservices, the database can't be shared over the network. For our monolithic Node.js server, this is fine.<\/p>\n\n<p><strong>4. Schema changes require iteration.<\/strong><br>\nApplying a migration to 200 databases takes 200x longer than one Postgres migration. In practice, each migration takes &lt; 10ms, so 200 databases complete in under 2 seconds. Not a real problem at our scale.<\/p>\n\n<h2>\n  \n  \n  When to choose SQLite for SaaS\n<\/h2>\n\n<p>SQLite per tenant works when:<\/p>\n\n<ul>\n<li>\n<strong>Tenants are isolated.<\/strong> No cross-tenant queries needed.<\/li>\n<li>\n<strong>Write concurrency is low per tenant.<\/strong> One writer at a time per database.<\/li>\n<li>\n<strong>Data volume per tenant is moderate.<\/strong> Under 1GB per database.<\/li>\n<li>\n<strong>Deployment is single-server.<\/strong> No need for database replication.<\/li>\n<li>\n<strong>Operational simplicity matters.<\/strong> No DBA, no database server management.<\/li>\n<\/ul>\n\n<p>SQLite per tenant breaks when:<\/p>\n\n<ul>\n<li>Tenants need to query each other's data<\/li>\n<li>Multiple processes write to the same tenant concurrently<\/li>\n<li>You need distributed transactions<\/li>\n<li>Individual databases exceed ~10GB<\/li>\n<li>You need real-time read replicas<\/li>\n<\/ul>\n\n<p>For HelperX, every checkbox in the \"works when\" column was checked. A year later, the database layer is the most boring part of the system \u2014 exactly what you want from infrastructure.<\/p>\n\n\n\n\n<p><em><a href=\"https:\/\/helperx.app\" rel=\"noopener noreferrer\">HelperX<\/a> runs on SQLite with per-slot isolation \u2014 zero database management, sub-millisecond queries. Try it free for 30 days.<\/em><\/p>\n\n","category":["database","node","architecture","saas"]},{"title":"xZeroProtect 1.1.1 \u2014 Smarter Defaults, Cleaner Rules, Real Visitor Tracking","pubDate":"Sun, 07 Jun 2026 19:11:25 +0000","link":"https:\/\/dev.to\/benkhalife\/xzeroprotect-111-smarter-defaults-cleaner-rules-real-visitor-tracking-5db3","guid":"https:\/\/dev.to\/benkhalife\/xzeroprotect-111-smarter-defaults-cleaner-rules-real-visitor-tracking-5db3","description":"<p>After using xZeroProtect across several projects, I kept running into the same friction points \u2014 default rules that blocked legitimate API clients, a <code>.php<\/code> extension rule that silently broke non-routed apps, and an auto-ban threshold that was a little too eager. Version 1.1.1 fixes all of that, and adds something new.<\/p>\n\n\n\n\n<h2>\n  \n  \n  What is xZeroProtect?\n<\/h2>\n\n<p>A lightweight, file-based PHP 8 firewall library. No database, no Redis, no external service. Drop it into any PHP project, call <code>run()<\/code>, and it handles the rest \u2014 rate limiting, IP banning, payload scanning, bot detection, and crawler verification.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>composer require webrium\/xzeroprotect\n<\/code><\/pre>\n\n<\/div>\n\n\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight php\"><code><span class=\"kn\">use<\/span> <span class=\"nc\">Webrium\\XZeroProtect\\XZeroProtect<\/span><span class=\"p\">;<\/span>\n\n<span class=\"nv\">$firewall<\/span> <span class=\"o\">=<\/span> <span class=\"nc\">XZeroProtect<\/span><span class=\"o\">::<\/span><span class=\"nf\">init<\/span><span class=\"p\">();<\/span>\n<span class=\"nv\">$firewall<\/span><span class=\"o\">-&gt;<\/span><span class=\"nf\">run<\/span><span class=\"p\">();<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>That's the whole setup for most projects.<\/p>\n\n\n\n\n<h2>\n  \n  \n  What changed in 1.1.1\n<\/h2>\n\n<h3>\n  \n  \n  Smarter default blocked agents\n<\/h3>\n\n<p>The previous release blocked <code>curl\/<\/code>, <code>wget\/<\/code>, <code>python-requests<\/code>, and <code>go-http-client<\/code> out of the box. That made sense for public-facing websites, but caused real problems for anyone exposing an API \u2014 every legitimate client using those libraries got blocked on install.<\/p>\n\n<p>These four are no longer in the default list. If your site doesn't serve an API and you want to block them, it's one line:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight php\"><code><span class=\"nv\">$firewall<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">patterns<\/span><span class=\"o\">-&gt;<\/span><span class=\"nf\">addAgent<\/span><span class=\"p\">(<\/span><span class=\"s1\">'curl\/'<\/span><span class=\"p\">);<\/span>\n<span class=\"nv\">$firewall<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">patterns<\/span><span class=\"o\">-&gt;<\/span><span class=\"nf\">addAgent<\/span><span class=\"p\">(<\/span><span class=\"s1\">'wget\/'<\/span><span class=\"p\">);<\/span>\n<span class=\"nv\">$firewall<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">patterns<\/span><span class=\"o\">-&gt;<\/span><span class=\"nf\">addAgent<\/span><span class=\"p\">(<\/span><span class=\"s1\">'python-requests'<\/span><span class=\"p\">);<\/span>\n<span class=\"nv\">$firewall<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">patterns<\/span><span class=\"o\">-&gt;<\/span><span class=\"nf\">addAgent<\/span><span class=\"p\">(<\/span><span class=\"s1\">'go-http-client'<\/span><span class=\"p\">);<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p><code>libwww-perl<\/code> and <code>lwp-trivial<\/code> are still blocked by default \u2014 there's no modern legitimate use case for those.<\/p>\n\n\n\n\n<h3>\n  \n  \n  <code>.php<\/code> extension no longer blocked by default\n<\/h3>\n\n<p>The old default blocked any URL containing <code>.php<\/code>. The intention was good \u2014 modern routed applications (Laravel, Symfony, Slim) have no public <code>.php<\/code> files, so blocking the extension stops a lot of scanner noise.<\/p>\n\n<p>The problem: anyone running a traditional PHP app, or a CMS like WordPress, would have all their pages blocked immediately after installation.<\/p>\n\n<p><code>.php<\/code> is now opt-in:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight php\"><code><span class=\"c1\">\/\/ Only add this if your app uses modern routing<\/span>\n<span class=\"c1\">\/\/ and no public .php files exist<\/span>\n<span class=\"nv\">$firewall<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">patterns<\/span><span class=\"o\">-&gt;<\/span><span class=\"nf\">addPath<\/span><span class=\"p\">(<\/span><span class=\"s1\">'.php'<\/span><span class=\"p\">);<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n\n\n\n<h3>\n  \n  \n  Auto-ban threshold raised from 5 to 10\n<\/h3>\n\n<p>The previous threshold of 5 violations before a ban was aggressive. A real user hitting a few 404s, a misconfigured uptime monitor, or a developer testing locally could end up banned for 24 hours.<\/p>\n\n<p>The new default is 10 \u2014 still firm enough to catch actual scanners and brute-force attempts, but with enough room to avoid false positives on legitimate traffic.<\/p>\n\n\n\n\n<h3>\n  \n  \n  New: <code>VisitInfo<\/code> and real visitor tracking\n<\/h3>\n\n<p>This release introduces opt-in visitor tracking. After all firewall checks pass, you can record the visit \u2014 and since the firewall has already filtered out bots and scanners, what gets recorded is real human traffic.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight php\"><code><span class=\"kn\">use<\/span> <span class=\"nc\">Webrium\\XZeroProtect\\VisitInfo<\/span><span class=\"p\">;<\/span>\n\n<span class=\"nv\">$firewall<\/span><span class=\"o\">-&gt;<\/span><span class=\"nf\">enableTracking<\/span><span class=\"p\">(<\/span><span class=\"k\">function<\/span> <span class=\"p\">(<\/span><span class=\"kt\">VisitInfo<\/span> <span class=\"nv\">$visit<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n    <span class=\"c1\">\/\/ Store however you like \u2014 the library doesn't care<\/span>\n    <span class=\"nv\">$db<\/span><span class=\"o\">-&gt;<\/span><span class=\"nf\">insert<\/span><span class=\"p\">(<\/span><span class=\"s1\">'visits'<\/span><span class=\"p\">,<\/span> <span class=\"nv\">$visit<\/span><span class=\"o\">-&gt;<\/span><span class=\"nf\">toArray<\/span><span class=\"p\">());<\/span>\n<span class=\"p\">});<\/span>\n\n<span class=\"nv\">$firewall<\/span><span class=\"o\">-&gt;<\/span><span class=\"nf\">run<\/span><span class=\"p\">();<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>The <code>VisitInfo<\/code> object gives you everything you need:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight php\"><code><span class=\"nv\">$visit<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">ip<\/span>\n<span class=\"nv\">$visit<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">path<\/span>\n<span class=\"nv\">$visit<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">method<\/span>\n<span class=\"nv\">$visit<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">fingerprint<\/span>      <span class=\"c1\">\/\/ daily SHA-256 hash \u2014 for unique visitor counting<\/span>\n<span class=\"nv\">$visit<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">device<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">browser<\/span>  <span class=\"c1\">\/\/ 'Chrome', 'Firefox', 'Safari' ...<\/span>\n<span class=\"nv\">$visit<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">device<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">os<\/span>       <span class=\"c1\">\/\/ 'Windows', 'macOS', 'Android', 'iOS' ...<\/span>\n<span class=\"nv\">$visit<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">device<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">type<\/span>     <span class=\"c1\">\/\/ 'desktop' | 'mobile' | 'tablet'<\/span>\n<span class=\"nv\">$visit<\/span><span class=\"o\">-&gt;<\/span><span class=\"nb\">date<\/span><span class=\"p\">()<\/span>           <span class=\"c1\">\/\/ formatted timestamp<\/span>\n<span class=\"nv\">$visit<\/span><span class=\"o\">-&gt;<\/span><span class=\"nf\">toArray<\/span><span class=\"p\">()<\/span>        <span class=\"c1\">\/\/ flat array ready for DB insert<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Tracking is disabled by default \u2014 you enable it explicitly. The library stays zero-dependency; how and where you store data is entirely up to you.<\/p>\n\n<p>I'll be writing a dedicated post covering the full tracking API, unique visitor fingerprinting, and some practical analytics patterns. This is just the introduction.<\/p>\n\n\n\n\n<h2>\n  \n  \n  What didn't change\n<\/h2>\n\n<p>The core API is identical. No breaking changes. If you were on 1.0.x, updating is safe:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>composer update webrium\/xzeroprotect\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>All existing configuration, custom rules, IP bans, and rate limit data carry over.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Links\n<\/h2>\n\n<ul>\n<li>\n<strong>GitHub:<\/strong> <a href=\"https:\/\/github.com\/webrium\/xzeroprotect\" rel=\"noopener noreferrer\">github.com\/webrium\/xzeroprotect<\/a>\n<\/li>\n<li>\n<strong>Packagist:<\/strong> <a href=\"https:\/\/packagist.org\/packages\/webrium\/xzeroprotect\" rel=\"noopener noreferrer\">packagist.org\/packages\/webrium\/xzeroprotect<\/a>\n<\/li>\n<li>\n<strong>Changelog:<\/strong> <a href=\"https:\/\/github.com\/webrium\/xzeroprotect\/releases\/tag\/1.1.1\" rel=\"noopener noreferrer\">github.com\/webrium\/xzeroprotect\/releases\/tag\/1.1.1<\/a>\n<\/li>\n<\/ul>\n\n<p>Feedback and issues welcome on GitHub.<\/p>\n\n","category":["php","security","webdev","opensource"]},{"title":"Generating JSON Schema from PHP DTOs with Symfony Serializer awareness","pubDate":"Sun, 07 Jun 2026 19:08:20 +0000","link":"https:\/\/dev.to\/antonioturdo\/generating-json-schema-from-php-dtos-with-symfony-serializer-awareness-459o","guid":"https:\/\/dev.to\/antonioturdo\/generating-json-schema-from-php-dtos-with-symfony-serializer-awareness-459o","description":"<p>A PHP project that serializes and deserializes DTOs often needs a JSON Schema for them \u2014 for an LLM's structured output, for API documentation, or to validate incoming payloads. <\/p>\n\n<p>Writing it by hand works until the code changes. With Symfony Serializer, the JSON shape can shift without the PHP types changing at all \u2014 a new serialization group, a <code>#[SerializedName]<\/code>, a discriminator \u2014 and the hand-written schema no longer matches what the serializer produces.<\/p>\n\n<p><a href=\"https:\/\/github.com\/antonioturdo\/json-schema-extractor\" rel=\"noopener noreferrer\">json-schema-extractor<\/a> is a PHP library that generates JSON Schema from DTOs by reading the metadata they already carry \u2014 native types, PHPDoc, and serializer configuration \u2014 so the schema stays in sync with the code. It supports plain <code>json_encode<\/code>\/<code>JsonSerializable<\/code> and Symfony Serializer; this article focuses on the latter.<\/p>\n\n\n\n\n<h2>\n  \n  \n  The problem in concrete terms\n<\/h2>\n\n<p>Take a simple DTO:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight php\"><code><span class=\"k\">final<\/span> <span class=\"kd\">class<\/span> <span class=\"nc\">OrderSummary<\/span>\n<span class=\"p\">{<\/span>\n    <span class=\"k\">public<\/span> <span class=\"k\">function<\/span> <span class=\"n\">__construct<\/span><span class=\"p\">(<\/span>\n        <span class=\"na\">#[Groups(['public'])]<\/span>\n        <span class=\"na\">#[SerializedName('order_id')]<\/span>\n        <span class=\"k\">public<\/span> <span class=\"k\">readonly<\/span> <span class=\"kt\">string<\/span> <span class=\"nv\">$id<\/span><span class=\"p\">,<\/span>\n\n        <span class=\"na\">#[Groups(['public'])]<\/span>\n        <span class=\"k\">public<\/span> <span class=\"k\">readonly<\/span> <span class=\"kt\">Money<\/span> <span class=\"nv\">$total<\/span><span class=\"p\">,<\/span>\n\n        <span class=\"na\">#[Groups(['internal'])]<\/span>\n        <span class=\"k\">public<\/span> <span class=\"k\">readonly<\/span> <span class=\"kt\">string<\/span> <span class=\"nv\">$internalNote<\/span><span class=\"p\">,<\/span>\n    <span class=\"p\">)<\/span> <span class=\"p\">{}<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>When Symfony Serializer renders this with the <code>public<\/code> group, the JSON output is:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight json\"><code><span class=\"p\">{<\/span><span class=\"w\"> \n  <\/span><span class=\"nl\">\"order_id\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">\"ORD-1042\"<\/span><span class=\"p\">,<\/span><span class=\"w\"> \n  <\/span><span class=\"nl\">\"total\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span><span class=\"w\"> \n    <\/span><span class=\"nl\">\"amount\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"mi\">4999<\/span><span class=\"p\">,<\/span><span class=\"w\"> \n    <\/span><span class=\"nl\">\"currency\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">\"EUR\"<\/span><span class=\"w\"> \n  <\/span><span class=\"p\">}<\/span><span class=\"w\"> \n<\/span><span class=\"p\">}<\/span><span class=\"w\">\n<\/span><\/code><\/pre>\n\n<\/div>\n\n\n\n<p><code>internalNote<\/code> is absent. <code>id<\/code> is renamed. <code>total<\/code> is a nested object. A hand-written schema that matches this needs to know about groups, serialized names, and how <code>Money<\/code> is normalized. If any of those change, you update them in two places.<\/p>\n\n\n\n\n<h2>\n  \n  \n  How the library works\n<\/h2>\n\n<p>The extraction pipeline has four phases, each with a clear responsibility:<\/p>\n\n<p><strong>1. Discover<\/strong> \u2014 <code>ReflectionDiscoverer<\/code> reads the class with reflection: property names, native PHP types, visibility. No dependencies required.<\/p>\n\n<p><strong>2. Enrich<\/strong> \u2014 one or more enrichers augment the PHP model with metadata reflection alone can't see. <code>PhpStanEnricher<\/code> and <code>PhpDocumentorEnricher<\/code> both read PHPDoc (<code>@var list&lt;string&gt;<\/code>, <code>@var array{name: string, age: int}<\/code>, generics, descriptions, deprecation), via different parsers. <code>SymfonyValidationEnricher<\/code> maps Symfony Validator constraints (<code>NotBlank<\/code>, <code>Length<\/code>, <code>Range<\/code>\u2026) to their JSON Schema equivalents \u2014 appropriate when the application actually validates the objects against those constraints, so the schema's guarantees hold for the real data. Enrichers are optional and composable.<\/p>\n\n<p><strong>3. Project<\/strong> \u2014 a serialization strategy converts the enriched PHP model into the <em>serialized shape<\/em>: the JSON-facing view of the class. The Symfony Serializer support is implemented in this phase.<\/p>\n\n<p><strong>4. Map<\/strong> \u2014 <code>StandardJsonSchemaMapper<\/code> folds the projected shape into a JSON Schema document, handling <code>$ref<\/code>, reusable definitions, dialect (<code>draft-7<\/code> or <code>2020-12<\/code>), and union semantics.<\/p>\n\n<p>The four phases are wired together by <code>SchemaExtractor<\/code>, the entry point you call to produce a schema.<\/p>\n\n<p>Each phase is defined by an interface \u2014 <code>DiscovererInterface<\/code>, <code>EnricherInterface<\/code>, <code>SerializationStrategyInterface<\/code>, <code>JsonSchemaMapperInterface<\/code> \u2014 so every component can be swapped or extended. You can plug in your own discoverer, enricher, strategy, or mapper without touching the rest.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Installation\n<\/h2>\n\n<p>Install the library, then add only the optional packages your chosen components need:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>composer require zeusi\/json-schema-extractor\n\n<span class=\"c\"># optional, depending on what you enable:<\/span>\ncomposer require phpstan\/phpdoc-parser              <span class=\"c\"># for PhpStanEnricher<\/span>\ncomposer require phpdocumentor\/reflection-docblock  <span class=\"c\"># for PhpDocumentorEnricher<\/span>\ncomposer require symfony\/validator                  <span class=\"c\"># for SymfonyValidationEnricher<\/span>\ncomposer require symfony\/serializer                 <span class=\"c\"># for SymfonySerializerStrategy<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>The core package itself has no mandatory dependencies.<\/p>\n\n\n\n\n<h2>\n  \n  \n  A minimal extractor\n<\/h2>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight php\"><code><span class=\"kn\">use<\/span> <span class=\"nc\">Zeusi\\JsonSchemaExtractor\\Discoverer\\ReflectionDiscoverer<\/span><span class=\"p\">;<\/span>\n<span class=\"kn\">use<\/span> <span class=\"nc\">Zeusi\\JsonSchemaExtractor\\Enricher\\PhpStanEnricher<\/span><span class=\"p\">;<\/span>\n<span class=\"kn\">use<\/span> <span class=\"nc\">Zeusi\\JsonSchemaExtractor\\Mapper\\StandardJsonSchemaMapper<\/span><span class=\"p\">;<\/span>\n<span class=\"kn\">use<\/span> <span class=\"nc\">Zeusi\\JsonSchemaExtractor\\SchemaExtractor<\/span><span class=\"p\">;<\/span>\n<span class=\"kn\">use<\/span> <span class=\"nc\">Zeusi\\JsonSchemaExtractor\\Serialization\\JsonEncodeSerializationStrategy<\/span><span class=\"p\">;<\/span>\n\n<span class=\"nv\">$extractor<\/span> <span class=\"o\">=<\/span> <span class=\"k\">new<\/span> <span class=\"nc\">SchemaExtractor<\/span><span class=\"p\">(<\/span>\n    <span class=\"k\">new<\/span> <span class=\"nc\">ReflectionDiscoverer<\/span><span class=\"p\">(),<\/span>\n    <span class=\"p\">[<\/span><span class=\"k\">new<\/span> <span class=\"nc\">PhpStanEnricher<\/span><span class=\"p\">()],<\/span>\n    <span class=\"k\">new<\/span> <span class=\"nc\">JsonEncodeSerializationStrategy<\/span><span class=\"p\">(),<\/span>\n    <span class=\"k\">new<\/span> <span class=\"nc\">StandardJsonSchemaMapper<\/span><span class=\"p\">(),<\/span>\n<span class=\"p\">);<\/span>\n\n<span class=\"nv\">$schema<\/span> <span class=\"o\">=<\/span> <span class=\"nv\">$extractor<\/span><span class=\"o\">-&gt;<\/span><span class=\"nb\">extract<\/span><span class=\"p\">(<\/span><span class=\"nc\">OrderSummary<\/span><span class=\"o\">::<\/span><span class=\"n\">class<\/span><span class=\"p\">);<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>This gives you a schema based on native PHP types and PHPDoc. <code>JsonEncodeSerializationStrategy<\/code> is the right choice when your JSON is produced by <code>json_encode()<\/code> or <code>JsonSerializable<\/code> (in that case, the shape is read from <code>jsonSerialize()<\/code>'s return type and PHPDoc, since its body is opaque to static analysis).<\/p>\n\n<p>For Symfony Serializer, you swap the strategy.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Adding Symfony Serializer awareness\n<\/h2>\n\n<p>To make the schema follow Symfony Serializer instead of <code>json_encode()<\/code>, swap in <code>SymfonySerializerStrategy<\/code>. Runtime serializer context \u2014 serialization groups, for example \u2014 is passed to <code>extract()<\/code> through an <code>ExtractionContext<\/code>, which is optional:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight php\"><code><span class=\"kn\">use<\/span> <span class=\"nc\">Symfony\\Component\\Serializer\\Mapping\\Factory\\ClassMetadataFactory<\/span><span class=\"p\">;<\/span>\n<span class=\"kn\">use<\/span> <span class=\"nc\">Symfony\\Component\\Serializer\\Mapping\\Loader\\AttributeLoader<\/span><span class=\"p\">;<\/span>\n<span class=\"kn\">use<\/span> <span class=\"nc\">Zeusi\\JsonSchemaExtractor\\Context\\ExtractionContext<\/span><span class=\"p\">;<\/span>\n<span class=\"kn\">use<\/span> <span class=\"nc\">Zeusi\\JsonSchemaExtractor\\Context\\SymfonySerializerContext<\/span><span class=\"p\">;<\/span>\n<span class=\"kn\">use<\/span> <span class=\"nc\">Zeusi\\JsonSchemaExtractor\\Serialization\\SymfonySerializerStrategy<\/span><span class=\"p\">;<\/span>\n\n<span class=\"nv\">$strategy<\/span> <span class=\"o\">=<\/span> <span class=\"k\">new<\/span> <span class=\"nc\">SymfonySerializerStrategy<\/span><span class=\"p\">(<\/span>\n    <span class=\"k\">new<\/span> <span class=\"nc\">ClassMetadataFactory<\/span><span class=\"p\">(<\/span><span class=\"k\">new<\/span> <span class=\"nc\">AttributeLoader<\/span><span class=\"p\">()),<\/span>\n<span class=\"p\">);<\/span>\n\n<span class=\"nv\">$extractor<\/span> <span class=\"o\">=<\/span> <span class=\"k\">new<\/span> <span class=\"nc\">SchemaExtractor<\/span><span class=\"p\">(<\/span>\n    <span class=\"k\">new<\/span> <span class=\"nc\">ReflectionDiscoverer<\/span><span class=\"p\">(),<\/span>\n    <span class=\"p\">[<\/span><span class=\"k\">new<\/span> <span class=\"nc\">PhpStanEnricher<\/span><span class=\"p\">()],<\/span>\n    <span class=\"nv\">$strategy<\/span><span class=\"p\">,<\/span>\n    <span class=\"k\">new<\/span> <span class=\"nc\">StandardJsonSchemaMapper<\/span><span class=\"p\">(),<\/span>\n<span class=\"p\">);<\/span>\n\n<span class=\"c1\">\/\/ Optional: a context carrying the runtime serializer settings (here, the \"public\" group).<\/span>\n<span class=\"nv\">$context<\/span> <span class=\"o\">=<\/span> <span class=\"p\">(<\/span><span class=\"k\">new<\/span> <span class=\"nc\">ExtractionContext<\/span><span class=\"p\">())<\/span><span class=\"o\">-&gt;<\/span><span class=\"nf\">with<\/span><span class=\"p\">(<\/span><span class=\"k\">new<\/span> <span class=\"nc\">SymfonySerializerContext<\/span><span class=\"p\">([<\/span>\n    <span class=\"s1\">'groups'<\/span> <span class=\"o\">=&gt;<\/span> <span class=\"p\">[<\/span><span class=\"s1\">'public'<\/span><span class=\"p\">],<\/span>\n<span class=\"p\">]));<\/span>\n\n<span class=\"nv\">$schema<\/span> <span class=\"o\">=<\/span> <span class=\"nv\">$extractor<\/span><span class=\"o\">-&gt;<\/span><span class=\"nb\">extract<\/span><span class=\"p\">(<\/span><span class=\"nc\">OrderSummary<\/span><span class=\"o\">::<\/span><span class=\"n\">class<\/span><span class=\"p\">,<\/span> <span class=\"nv\">$context<\/span><span class=\"p\">);<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>For <code>OrderSummary<\/code> with the <code>public<\/code> group, the schema matches what Symfony Serializer produces: <code>internalNote<\/code> is absent, and <code>id<\/code> appears as <code>order_id<\/code>.<\/p>\n\n<p>The strategy reads the same inputs Symfony Serializer reads at runtime \u2014 both the attributes declared on the DTO and the options passed in the serializer context (the array shown above, where <code>groups<\/code> lives):<\/p>\n\n<ul>\n<li>\n<strong><code>#[SerializedName]<\/code> \/ name converters<\/strong> \u2192 property keys are renamed in the schema.<\/li>\n<li>\n<strong>Serialization groups<\/strong> (<code>groups<\/code> context option) \u2192 only properties in the selected groups appear.<\/li>\n<li>\n<strong><code>ignored_attributes<\/code><\/strong> (context option) \u2192 the listed properties are excluded.<\/li>\n<li>\n<strong><code>attributes<\/code><\/strong> (context option) \u2192 restricts the schema to the listed attributes, with per-property nested views for class-backed ones.<\/li>\n<li>\n<strong>Discriminator maps<\/strong> (<code>#[DiscriminatorMap]<\/code>) \u2192 a base type expands to a <code>oneOf<\/code> over its mapped subtypes, each tagged with the discriminator field; a concrete subtype is a single object with that field fixed to its key.<\/li>\n<li>\n<strong>Known normalizers<\/strong> (by type) \u2192 <code>DateTimeInterface<\/code> becomes <code>{ type: string, format: date-time }<\/code>, Symfony UIDs become <code>{ type: string, format: uuid }<\/code>, and so on.<\/li>\n<li>\n<strong><code>skip_null_values<\/code><\/strong> (context option) \u2192 nullable properties become optional in the schema.<\/li>\n<\/ul>\n\n\n\n\n<h2>\n  \n  \n  What it does not model\n<\/h2>\n\n<p>The strategy reads <strong>static metadata<\/strong> \u2014 attributes and types \u2014 so it cannot mirror everything Symfony Serializer does at runtime. It is not a 1:1 mapping of the serializer's behaviour. The gaps worth knowing about:<\/p>\n\n<ul>\n<li>\n<strong><code>preserve_empty_objects<\/code><\/strong> (context option) \u2192 serializes an empty collection as <code>{}<\/code> instead of <code>[]<\/code>; whether it applies depends on the runtime value, which static analysis can't see.<\/li>\n<li>\n<strong><code>skip_uninitialized_values<\/code><\/strong> (context option) \u2192 omits typed properties that were never assigned; that's a fact about the object instance at runtime, not about the class.<\/li>\n<li>\n<strong>Custom normalizers<\/strong> outside the known set \u2192 their output shape is defined in application code, so it can't be inferred.<\/li>\n<li>\n<strong><code>max_depth_handler<\/code> \/ <code>circular_reference_handler<\/code><\/strong> (context options) \u2192 callables that replace a node when the depth limit or a cycle is hit; their output is arbitrary application code.<\/li>\n<li>\n<strong><code>#[MaxDepth]<\/code><\/strong> \u2192 not modeled as a tightened schema, and doesn't need to be: recursion is already broken with a <code>$ref<\/code>, and a depth-bounded payload is a valid instance of that (looser) recursive schema.<\/li>\n<li>\n<strong>Interfaces \/ polymorphic base types<\/strong> \u2192 an interface has no single concrete shape, so it can't be resolved on its own. A Symfony <code>#[DiscriminatorMap]<\/code> turns it into a <code>oneOf<\/code> when the payload carries a type field; otherwise a custom strategy or enricher can supply the concrete shapes.<\/li>\n<\/ul>\n\n<p>When a case isn't covered, the serialization strategy is the extension point: implement <code>SerializationStrategyInterface<\/code>, or decorate <code>SymfonySerializerStrategy<\/code>, and handle it there.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Reusable definitions and $ref\n<\/h2>\n\n<p>By default, class-backed nested types are emitted once under <code>definitions<\/code> (Draft-7) or <code>$defs<\/code> (2020-12) and referenced with <code>$ref<\/code> everywhere they are used. If you prefer the nested schemas expanded at the point of use instead, switch to <code>ClassReferenceStrategy::Inline<\/code>:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight php\"><code><span class=\"kn\">use<\/span> <span class=\"nc\">Zeusi\\JsonSchemaExtractor\\Mapper\\ClassReferenceStrategy<\/span><span class=\"p\">;<\/span>\n<span class=\"kn\">use<\/span> <span class=\"nc\">Zeusi\\JsonSchemaExtractor\\Mapper\\StandardJsonSchemaMapper<\/span><span class=\"p\">;<\/span>\n<span class=\"kn\">use<\/span> <span class=\"nc\">Zeusi\\JsonSchemaExtractor\\Mapper\\StandardJsonSchemaMapperOptions<\/span><span class=\"p\">;<\/span>\n\n<span class=\"nv\">$mapper<\/span> <span class=\"o\">=<\/span> <span class=\"k\">new<\/span> <span class=\"nc\">StandardJsonSchemaMapper<\/span><span class=\"p\">(<\/span><span class=\"k\">new<\/span> <span class=\"nc\">StandardJsonSchemaMapperOptions<\/span><span class=\"p\">(<\/span>\n    <span class=\"n\">classReferenceStrategy<\/span><span class=\"o\">:<\/span> <span class=\"nc\">ClassReferenceStrategy<\/span><span class=\"o\">::<\/span><span class=\"nc\">Inline<\/span><span class=\"p\">,<\/span>\n<span class=\"p\">));<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Circular references are handled automatically either way: a self-referential class produces a <code>$ref<\/code> back to the root (<code>#<\/code>) or to the relevant definition, rather than an infinite expansion.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Symfony Bundle\n<\/h2>\n\n<p>If you use the Symfony framework, a bundle registers the built-in components as services and wires the extractor into the container. Its main convenience is reusing the <strong>Symfony Serializer and Validator<\/strong> services your application already has \u2014 so you don't assemble a <code>ClassMetadataFactory<\/code>, a validator, and the strategy\/enricher wiring by hand.<\/p>\n\n<p>You declare one or more extractor pipelines, choosing the strategy that matches how your app serializes:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight yaml\"><code><span class=\"c1\"># config\/packages\/json_schema_extractor.yaml<\/span>\n<span class=\"na\">json_schema_extractor<\/span><span class=\"pi\">:<\/span>\n  <span class=\"na\">default_extractor<\/span><span class=\"pi\">:<\/span> <span class=\"s\">api<\/span>\n  <span class=\"na\">extractors<\/span><span class=\"pi\">:<\/span>\n    <span class=\"na\">api<\/span><span class=\"pi\">:<\/span>\n      <span class=\"na\">enrichers<\/span><span class=\"pi\">:<\/span>\n        <span class=\"pi\">-<\/span> <span class=\"s\">json_schema_extractor.enricher.phpstan<\/span>\n      <span class=\"na\">serialization<\/span><span class=\"pi\">:<\/span> <span class=\"s\">json_schema_extractor.serialization.symfony_serializer<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p><code>SchemaExtractor<\/code> is aliased to the default extractor, so you inject it directly:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight php\"><code><span class=\"k\">public<\/span> <span class=\"k\">function<\/span> <span class=\"n\">__construct<\/span><span class=\"p\">(<\/span>\n    <span class=\"k\">private<\/span> <span class=\"k\">readonly<\/span> <span class=\"kt\">SchemaExtractor<\/span> <span class=\"nv\">$extractor<\/span><span class=\"p\">,<\/span>\n<span class=\"p\">)<\/span> <span class=\"p\">{}<\/span>\n\n<span class=\"c1\">\/\/ ...<\/span>\n<span class=\"nv\">$schema<\/span> <span class=\"o\">=<\/span> <span class=\"nv\">$this<\/span><span class=\"o\">-&gt;<\/span><span class=\"n\">extractor<\/span><span class=\"o\">-&gt;<\/span><span class=\"nb\">extract<\/span><span class=\"p\">(<\/span><span class=\"nc\">OrderSummary<\/span><span class=\"o\">::<\/span><span class=\"n\">class<\/span><span class=\"p\">);<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>See the <a href=\"https:\/\/github.com\/antonioturdo\/json-schema-extractor\/blob\/main\/docs\/symfony-bundle.md\" rel=\"noopener noreferrer\">bundle documentation<\/a> for multiple pipelines, custom services, and the debug command.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Use case: structured output for LLMs\n<\/h2>\n\n<p>A common use today is structured output from an LLM. Most providers accept a JSON Schema as the contract for the model's response: you generate the schema from a DTO, send it with the request, and deserialize the response back into that same DTO.<\/p>\n\n<p>Since the schema comes from the DTO you deserialize into, the two stay in sync as the DTO changes.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Resources\n<\/h2>\n\n<p>The library is open source under the MIT license: <a href=\"https:\/\/github.com\/antonioturdo\/json-schema-extractor\" rel=\"noopener noreferrer\">github.com\/antonioturdo\/json-schema-extractor<\/a>. The documentation covers all enrichers, serialization strategies, mapper options, and the Symfony bundle in detail.<\/p>\n\n<p>If you are building something with it \u2014 structured output pipelines, AsyncAPI documentation, API contract testing \u2014 feedback and contributions are welcome.<\/p>\n\n","category":["php","symfony","jsonschema"]},{"title":"CycleTLS v2: Why We Ditched Axios for TLS Fingerprint-Safe HTTP","pubDate":"Sun, 07 Jun 2026 19:07:54 +0000","link":"https:\/\/dev.to\/helperx\/cycletls-v2-why-we-ditched-axios-for-tls-fingerprint-safe-http-50b7","guid":"https:\/\/dev.to\/helperx\/cycletls-v2-why-we-ditched-axios-for-tls-fingerprint-safe-http-50b7","description":"<p>When you're automating requests against platforms that actively detect bots, your HTTP library is a liability. We learned this the hard way \u2014 Axios was getting our requests fingerprinted and blocked within hours. Here's why we migrated to CycleTLS v2 and what it took to make it production-ready.<\/p>\n\n<h2>\n  \n  \n  The fingerprinting problem\n<\/h2>\n\n<p>Every HTTP client has a TLS fingerprint. When your browser connects to a server over HTTPS, the TLS handshake reveals:<\/p>\n\n<ul>\n<li>Supported cipher suites (and their order)<\/li>\n<li>TLS extensions<\/li>\n<li>Elliptic curve preferences<\/li>\n<li>ALPN protocols<\/li>\n<li>Signature algorithms<\/li>\n<\/ul>\n\n<p>This combination creates a fingerprint \u2014 called a JA3 hash \u2014 that uniquely identifies the client. Chrome has one fingerprint. Firefox has another. Node.js <code>https<\/code> module has yet another.<\/p>\n\n<p>And every bot-detection service knows what Node.js looks like.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code># Node.js 20 JA3 hash (via Axios\/https)\n769,4866-4867-4865-49196-49200-159-52393-52392-52394...\n\n# Chrome 124 JA3 hash\n769,4865-4866-4867-49195-49199-49196-49200-52393-52392...\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>The cipher suite order is different. The extensions list is different. It's trivial for a server to distinguish Node.js from a real browser \u2014 and block accordingly.<\/p>\n\n<h2>\n  \n  \n  What CycleTLS does differently\n<\/h2>\n\n<p>CycleTLS spawns a Go process that handles TLS handshakes using Go's <code>crypto\/tls<\/code> package, configured to mimic real browser fingerprints. The Node.js side communicates with this Go process via WebSocket.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510     WebSocket     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502   Node.js    \u2502 \u25c4\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba \u2502   Go binary  \u2502\n\u2502  (your app)  \u2502                   \u2502  (TLS proxy) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                   \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                          \u2502\n                                     TLS handshake\n                                   (Chrome fingerprint)\n                                          \u2502\n                                   \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                                   \u2502  Target API  \u2502\n                                   \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>The target server sees a Chrome-like TLS handshake. Your application logic stays in Node.js. The fingerprint problem is solved at the transport layer.<\/p>\n\n<h2>\n  \n  \n  Migrating from v1 to v2\n<\/h2>\n\n<p>CycleTLS v2 introduced breaking changes that improved reliability but required significant refactoring. Here's what changed and how we handled it.<\/p>\n\n<h3>\n  \n  \n  Connection lifecycle\n<\/h3>\n\n<p>In v1, you created a CycleTLS instance and it managed connections internally:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight javascript\"><code><span class=\"c1\">\/\/ v1 \u2014 implicit lifecycle<\/span>\n<span class=\"k\">import<\/span> <span class=\"nx\">CycleTLS<\/span> <span class=\"k\">from<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">cycletls<\/span><span class=\"dl\">'<\/span><span class=\"p\">;<\/span>\n\n<span class=\"kd\">const<\/span> <span class=\"nx\">cycleTLS<\/span> <span class=\"o\">=<\/span> <span class=\"k\">new<\/span> <span class=\"nc\">CycleTLS<\/span><span class=\"p\">();<\/span>\n<span class=\"kd\">const<\/span> <span class=\"nx\">response<\/span> <span class=\"o\">=<\/span> <span class=\"k\">await<\/span> <span class=\"nx\">cycleTLS<\/span><span class=\"p\">.<\/span><span class=\"nf\">get<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">https:\/\/api.example.com\/data<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span> <span class=\"p\">{<\/span>\n  <span class=\"na\">ja3<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">771,4865-4866-4867...<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n  <span class=\"na\">userAgent<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">Mozilla\/5.0...<\/span><span class=\"dl\">'<\/span>\n<span class=\"p\">});<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>In v2, connection management is explicit:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight javascript\"><code><span class=\"c1\">\/\/ v2 \u2014 explicit lifecycle<\/span>\n<span class=\"k\">import<\/span> <span class=\"p\">{<\/span> <span class=\"nx\">CycleTLS<\/span> <span class=\"p\">}<\/span> <span class=\"k\">from<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">cycletls<\/span><span class=\"dl\">'<\/span><span class=\"p\">;<\/span>\n\n<span class=\"kd\">const<\/span> <span class=\"nx\">cycleTLS<\/span> <span class=\"o\">=<\/span> <span class=\"k\">new<\/span> <span class=\"nc\">CycleTLS<\/span><span class=\"p\">();<\/span>\n<span class=\"k\">await<\/span> <span class=\"nx\">cycleTLS<\/span><span class=\"p\">.<\/span><span class=\"nf\">start<\/span><span class=\"p\">();<\/span>\n\n<span class=\"kd\">const<\/span> <span class=\"nx\">response<\/span> <span class=\"o\">=<\/span> <span class=\"k\">await<\/span> <span class=\"nx\">cycleTLS<\/span><span class=\"p\">.<\/span><span class=\"nf\">get<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">https:\/\/api.example.com\/data<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span> <span class=\"p\">{<\/span>\n  <span class=\"na\">ja3<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">771,4865-4866-4867...<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n  <span class=\"na\">userAgent<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">Mozilla\/5.0...<\/span><span class=\"dl\">'<\/span>\n<span class=\"p\">});<\/span>\n\n<span class=\"k\">await<\/span> <span class=\"nx\">cycleTLS<\/span><span class=\"p\">.<\/span><span class=\"nf\">exit<\/span><span class=\"p\">();<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>The <code>start()<\/code> and <code>exit()<\/code> methods give you control over the Go subprocess lifecycle. In v1, the subprocess could linger if your Node.js process crashed. In v2, you manage it explicitly.<\/p>\n\n<h3>\n  \n  \n  Error handling\n<\/h3>\n\n<p>V1 swallowed many errors silently. V2 throws properly:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight javascript\"><code><span class=\"k\">try<\/span> <span class=\"p\">{<\/span>\n  <span class=\"kd\">const<\/span> <span class=\"nx\">response<\/span> <span class=\"o\">=<\/span> <span class=\"k\">await<\/span> <span class=\"nx\">cycleTLS<\/span><span class=\"p\">.<\/span><span class=\"nf\">get<\/span><span class=\"p\">(<\/span><span class=\"nx\">url<\/span><span class=\"p\">,<\/span> <span class=\"nx\">options<\/span><span class=\"p\">);<\/span>\n  <span class=\"k\">if <\/span><span class=\"p\">(<\/span><span class=\"nx\">response<\/span><span class=\"p\">.<\/span><span class=\"nx\">status<\/span> <span class=\"o\">!==<\/span> <span class=\"mi\">200<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n    <span class=\"c1\">\/\/ v2 returns status codes consistently<\/span>\n    <span class=\"nf\">handleApiError<\/span><span class=\"p\">(<\/span><span class=\"nx\">response<\/span><span class=\"p\">.<\/span><span class=\"nx\">status<\/span><span class=\"p\">,<\/span> <span class=\"nx\">response<\/span><span class=\"p\">.<\/span><span class=\"nx\">body<\/span><span class=\"p\">);<\/span>\n  <span class=\"p\">}<\/span>\n<span class=\"p\">}<\/span> <span class=\"k\">catch <\/span><span class=\"p\">(<\/span><span class=\"nx\">err<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n  <span class=\"k\">if <\/span><span class=\"p\">(<\/span><span class=\"nx\">err<\/span><span class=\"p\">.<\/span><span class=\"nx\">message<\/span><span class=\"p\">.<\/span><span class=\"nf\">includes<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">connection refused<\/span><span class=\"dl\">'<\/span><span class=\"p\">))<\/span> <span class=\"p\">{<\/span>\n    <span class=\"c1\">\/\/ Go subprocess died \u2014 need to restart<\/span>\n    <span class=\"k\">await<\/span> <span class=\"nx\">cycleTLS<\/span><span class=\"p\">.<\/span><span class=\"nf\">exit<\/span><span class=\"p\">().<\/span><span class=\"k\">catch<\/span><span class=\"p\">(()<\/span> <span class=\"o\">=&gt;<\/span> <span class=\"p\">{});<\/span>\n    <span class=\"k\">await<\/span> <span class=\"nx\">cycleTLS<\/span><span class=\"p\">.<\/span><span class=\"nf\">start<\/span><span class=\"p\">();<\/span>\n    <span class=\"c1\">\/\/ retry...<\/span>\n  <span class=\"p\">}<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Per-account circuit breaker\n<\/h3>\n\n<p>The biggest addition in our v2 migration: a circuit breaker per account slot. If a slot's requests start failing consistently, the circuit opens and stops sending requests \u2014 preventing cascade failures.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight javascript\"><code><span class=\"kd\">class<\/span> <span class=\"nc\">SlotCircuitBreaker<\/span> <span class=\"p\">{<\/span>\n  <span class=\"nf\">constructor<\/span><span class=\"p\">(<\/span><span class=\"nx\">slotId<\/span><span class=\"p\">,<\/span> <span class=\"p\">{<\/span> <span class=\"nx\">threshold<\/span> <span class=\"o\">=<\/span> <span class=\"mi\">5<\/span><span class=\"p\">,<\/span> <span class=\"nx\">resetTimeout<\/span> <span class=\"o\">=<\/span> <span class=\"mi\">60000<\/span> <span class=\"p\">}<\/span> <span class=\"o\">=<\/span> <span class=\"p\">{})<\/span> <span class=\"p\">{<\/span>\n    <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">slotId<\/span> <span class=\"o\">=<\/span> <span class=\"nx\">slotId<\/span><span class=\"p\">;<\/span>\n    <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">failures<\/span> <span class=\"o\">=<\/span> <span class=\"mi\">0<\/span><span class=\"p\">;<\/span>\n    <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">threshold<\/span> <span class=\"o\">=<\/span> <span class=\"nx\">threshold<\/span><span class=\"p\">;<\/span>\n    <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">resetTimeout<\/span> <span class=\"o\">=<\/span> <span class=\"nx\">resetTimeout<\/span><span class=\"p\">;<\/span>\n    <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">state<\/span> <span class=\"o\">=<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">closed<\/span><span class=\"dl\">'<\/span><span class=\"p\">;<\/span> <span class=\"c1\">\/\/ closed | open | half-open<\/span>\n    <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">lastFailure<\/span> <span class=\"o\">=<\/span> <span class=\"kc\">null<\/span><span class=\"p\">;<\/span>\n  <span class=\"p\">}<\/span>\n\n  <span class=\"k\">async<\/span> <span class=\"nf\">execute<\/span><span class=\"p\">(<\/span><span class=\"nx\">fn<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n    <span class=\"k\">if <\/span><span class=\"p\">(<\/span><span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">state<\/span> <span class=\"o\">===<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">open<\/span><span class=\"dl\">'<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n      <span class=\"k\">if <\/span><span class=\"p\">(<\/span><span class=\"nb\">Date<\/span><span class=\"p\">.<\/span><span class=\"nf\">now<\/span><span class=\"p\">()<\/span> <span class=\"o\">-<\/span> <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">lastFailure<\/span> <span class=\"o\">&gt;<\/span> <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">resetTimeout<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n        <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">state<\/span> <span class=\"o\">=<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">half-open<\/span><span class=\"dl\">'<\/span><span class=\"p\">;<\/span>\n      <span class=\"p\">}<\/span> <span class=\"k\">else<\/span> <span class=\"p\">{<\/span>\n        <span class=\"k\">throw<\/span> <span class=\"k\">new<\/span> <span class=\"nc\">Error<\/span><span class=\"p\">(<\/span><span class=\"s2\">`Circuit open for slot <\/span><span class=\"p\">${<\/span><span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">slotId<\/span><span class=\"p\">}<\/span><span class=\"s2\">`<\/span><span class=\"p\">);<\/span>\n      <span class=\"p\">}<\/span>\n    <span class=\"p\">}<\/span>\n\n    <span class=\"k\">try<\/span> <span class=\"p\">{<\/span>\n      <span class=\"kd\">const<\/span> <span class=\"nx\">result<\/span> <span class=\"o\">=<\/span> <span class=\"k\">await<\/span> <span class=\"nf\">fn<\/span><span class=\"p\">();<\/span>\n      <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nf\">onSuccess<\/span><span class=\"p\">();<\/span>\n      <span class=\"k\">return<\/span> <span class=\"nx\">result<\/span><span class=\"p\">;<\/span>\n    <span class=\"p\">}<\/span> <span class=\"k\">catch <\/span><span class=\"p\">(<\/span><span class=\"nx\">err<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n      <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nf\">onFailure<\/span><span class=\"p\">();<\/span>\n      <span class=\"k\">throw<\/span> <span class=\"nx\">err<\/span><span class=\"p\">;<\/span>\n    <span class=\"p\">}<\/span>\n  <span class=\"p\">}<\/span>\n\n  <span class=\"nf\">onSuccess<\/span><span class=\"p\">()<\/span> <span class=\"p\">{<\/span>\n    <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">failures<\/span> <span class=\"o\">=<\/span> <span class=\"mi\">0<\/span><span class=\"p\">;<\/span>\n    <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">state<\/span> <span class=\"o\">=<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">closed<\/span><span class=\"dl\">'<\/span><span class=\"p\">;<\/span>\n  <span class=\"p\">}<\/span>\n\n  <span class=\"nf\">onFailure<\/span><span class=\"p\">()<\/span> <span class=\"p\">{<\/span>\n    <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">failures<\/span><span class=\"o\">++<\/span><span class=\"p\">;<\/span>\n    <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">lastFailure<\/span> <span class=\"o\">=<\/span> <span class=\"nb\">Date<\/span><span class=\"p\">.<\/span><span class=\"nf\">now<\/span><span class=\"p\">();<\/span>\n    <span class=\"k\">if <\/span><span class=\"p\">(<\/span><span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">failures<\/span> <span class=\"o\">&gt;=<\/span> <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">threshold<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n      <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">state<\/span> <span class=\"o\">=<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">open<\/span><span class=\"dl\">'<\/span><span class=\"p\">;<\/span>\n    <span class=\"p\">}<\/span>\n  <span class=\"p\">}<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Each slot gets its own circuit breaker. If slot A's proxy goes down, slot B continues operating. The circuit breaker prevents slot A from hammering a dead proxy and burning through rate limits.<\/p>\n\n<h2>\n  \n  \n  JA3 fingerprint management\n<\/h2>\n\n<p>We maintain a rotation of JA3 fingerprints matching current browser versions:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight javascript\"><code><span class=\"kd\">const<\/span> <span class=\"nx\">JA3_PROFILES<\/span> <span class=\"o\">=<\/span> <span class=\"p\">{<\/span>\n  <span class=\"na\">chrome_124<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n    <span class=\"na\">ja3<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">771,4865-4866-4867-49195-49199-49196-49200-52393-52392-52394-49327-49325-49315-49311-49245-49249-49239-49235-158-162-49267-49271-107-103-49268-49272-57-51-159-163-49312-49316-49246-49250-49240-49236-52393,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513-21,29-23-24,0<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n    <span class=\"na\">userAgent<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36...<\/span><span class=\"dl\">'<\/span>\n  <span class=\"p\">},<\/span>\n  <span class=\"na\">chrome_125<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span> <span class=\"cm\">\/* ... *\/<\/span> <span class=\"p\">},<\/span>\n  <span class=\"na\">firefox_126<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span> <span class=\"cm\">\/* ... *\/<\/span> <span class=\"p\">},<\/span>\n<span class=\"p\">};<\/span>\n\n<span class=\"kd\">function<\/span> <span class=\"nf\">getRandomProfile<\/span><span class=\"p\">()<\/span> <span class=\"p\">{<\/span>\n  <span class=\"kd\">const<\/span> <span class=\"nx\">keys<\/span> <span class=\"o\">=<\/span> <span class=\"nb\">Object<\/span><span class=\"p\">.<\/span><span class=\"nf\">keys<\/span><span class=\"p\">(<\/span><span class=\"nx\">JA3_PROFILES<\/span><span class=\"p\">);<\/span>\n  <span class=\"k\">return<\/span> <span class=\"nx\">JA3_PROFILES<\/span><span class=\"p\">[<\/span><span class=\"nx\">keys<\/span><span class=\"p\">[<\/span><span class=\"nb\">Math<\/span><span class=\"p\">.<\/span><span class=\"nf\">floor<\/span><span class=\"p\">(<\/span><span class=\"nb\">Math<\/span><span class=\"p\">.<\/span><span class=\"nf\">random<\/span><span class=\"p\">()<\/span> <span class=\"o\">*<\/span> <span class=\"nx\">keys<\/span><span class=\"p\">.<\/span><span class=\"nx\">length<\/span><span class=\"p\">)]];<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>We update these profiles monthly when major browser versions release. Stale fingerprints get detected \u2014 a Chrome 110 fingerprint in 2026 is as suspicious as a Node.js fingerprint.<\/p>\n\n<h2>\n  \n  \n  Performance comparison\n<\/h2>\n\n<p>After the migration, we benchmarked both approaches:<\/p>\n\n<div class=\"table-wrapper-paragraph\"><table>\n<thead>\n<tr>\n<th>Metric<\/th>\n<th>Axios + Node TLS<\/th>\n<th>CycleTLS v2<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Request latency (avg)<\/td>\n<td>180ms<\/td>\n<td>220ms<\/td>\n<\/tr>\n<tr>\n<td>Block rate (24h)<\/td>\n<td>34%<\/td>\n<td>0.8%<\/td>\n<\/tr>\n<tr>\n<td>Successful sessions (7d)<\/td>\n<td>62%<\/td>\n<td>97%<\/td>\n<\/tr>\n<tr>\n<td>Memory overhead<\/td>\n<td>45MB<\/td>\n<td>85MB (Go process)<\/td>\n<\/tr>\n<tr>\n<td>Crash recovery<\/td>\n<td>Manual restart<\/td>\n<td>Auto-restart via circuit breaker<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/div>\n\n<p>CycleTLS adds ~40ms latency and ~40MB memory for the Go subprocess. In return, our block rate dropped from 34% to under 1%. The tradeoff is obvious.<\/p>\n\n<h2>\n  \n  \n  Lessons learned\n<\/h2>\n\n<p><strong>1. Transport-layer fingerprinting is the first detection layer.<\/strong> You can have perfect request timing, realistic headers, and residential IPs \u2014 if your TLS fingerprint says \"Node.js,\" you're blocked before the request is parsed.<\/p>\n\n<p><strong>2. Library upgrades in critical paths need circuit breakers.<\/strong> We deployed v2 without a circuit breaker initially. The first time the Go subprocess crashed under load, all slots went down simultaneously. Circuit breakers per slot fixed this.<\/p>\n\n<p><strong>3. JA3 profiles have a shelf life.<\/strong> We got lazy about updating profiles for two months. Block rates crept up from 0.8% to 6% before we noticed. Monthly updates are non-negotiable.<\/p>\n\n<p><strong>4. Explicit lifecycle management is worth the boilerplate.<\/strong> V1's implicit management was convenient until it wasn't. V2's explicit <code>start()<\/code>\/<code>exit()<\/code> made debugging subprocess issues trivial.<\/p>\n\n<p><strong>5. Go + Node.js via WebSocket is a viable production pattern.<\/strong> We were skeptical about the multi-process architecture. After 6 months in production, the WebSocket bridge has been rock-solid. The Go binary handles what Go does best (TLS), and Node.js handles what it does best (async I\/O and application logic).<\/p>\n\n\n\n\n<p><em><a href=\"https:\/\/helperx.app\" rel=\"noopener noreferrer\">HelperX<\/a> uses CycleTLS v2 with per-account circuit breakers for fingerprint-safe automation. Try it free for 30 days.<\/em><\/p>\n\n","category":["node","security","automation","webdev"]},{"title":"Longest Consecutive Sequence","pubDate":"Sun, 07 Jun 2026 19:02:28 +0000","link":"https:\/\/dev.to\/jaspreet_singh_86ae1740ac\/longest-consecutive-sequence-3m0h","guid":"https:\/\/dev.to\/jaspreet_singh_86ae1740ac\/longest-consecutive-sequence-3m0h","description":"<p>\ud83d\udd17 <strong>Problem Link:<\/strong> <a href=\"https:\/\/leetcode.com\/problems\/longest-consecutive-sequence\/\" rel=\"noopener noreferrer\">https:\/\/leetcode.com\/problems\/longest-consecutive-sequence\/<\/a><\/p>\n\n<p>The <strong>Longest Consecutive Sequence<\/strong> problem asks us to find the length of the longest sequence of consecutive integers in an unsorted array.<\/p>\n\n<p>While the brute force solution repeatedly searches for the next consecutive number, the optimal solution uses hashing to identify sequence starting points and build sequences efficiently.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Brute Force Approach\n<\/h2>\n\n<h3>\n  \n  \n  Intuition\n<\/h3>\n\n<p>For every number, keep checking whether the next consecutive number exists in the array.<\/p>\n\n<p>If it exists, continue extending the sequence. Otherwise, stop and update the maximum length found so far.<\/p>\n\n<p>Since searching for a number requires scanning the entire array, this becomes inefficient for large inputs.<\/p>\n\n<h3>\n  \n  \n  Time &amp; Space Complexity\n<\/h3>\n\n<ul>\n<li>Time Complexity: <strong>O(N\u00b2)<\/strong>\n<\/li>\n<li>Space Complexity: <strong>O(1)<\/strong>\n<\/li>\n<\/ul>\n\n<h3>\n  \n  \n  Java Solution\n<\/h3>\n\n\n\n<p>```java id=\"0mpj18\"<br>\nclass Solution {<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>public int longestConsecutive(int[] nums) {\n\n    int longest = 0;\n\n    for(int num : nums) {\n\n        int current = num;\n        int length = 1;\n\n        while(contains(nums, current + 1)) {\n            current++;\n            length++;\n        }\n\n        longest = Math.max(longest, length);\n    }\n\n    return longest;\n}\n\nprivate boolean contains(int[] nums, int target) {\n\n    for(int num : nums) {\n        if(num == target) {\n            return true;\n        }\n    }\n\n    return false;\n}\n<\/code><\/pre>\n\n<\/div>\n<p>}<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>\n\n\n---\n\n## Optimizing the Solution\n\nThe key observation is:\n\nA number can only be the start of a sequence if its previous number does not exist.\n\nFor example:\n\n\n\n```text id=\"av9f5t\"\n[100,4,200,1,3,2]\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>Here:<\/p>\n\n<ul>\n<li>1 is a valid starting point.<\/li>\n<li>2 is not because 1 exists.<\/li>\n<li>3 is not because 2 exists.<\/li>\n<li>4 is not because 3 exists.<\/li>\n<\/ul>\n\n<p>Instead of starting a sequence from every element, we only start from valid sequence beginnings.<\/p>\n\n<p>This avoids redundant work and gives us a linear-time solution.<\/p>\n\n\n<h2>\n  \n  \n  Optimal Approach (HashMap)\n<\/h2>\n<h3>\n  \n  \n  Time &amp; Space Complexity\n<\/h3>\n\n<ul>\n<li>Time Complexity: <strong>O(N)<\/strong>\n<\/li>\n<li>Space Complexity: <strong>O(N)<\/strong>\n<\/li>\n<\/ul>\n<h3>\n  \n  \n  Java Solution\n<\/h3>\n\n\n\n<p>```java id=\"mr0t5l\"<br>\nclass Solution {<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>public int longestConsecutive(int[] nums) {\n\n    HashMap&lt;Integer, Boolean&gt; map = new HashMap&lt;&gt;();\n\n    for(int num : nums) {\n        map.put(num, true);\n    }\n\n    for(int num : nums) {\n\n        if(map.containsKey(num - 1)) {\n            map.put(num, false);\n        }\n    }\n\n    int maxLength = 0;\n\n    for(int num : nums) {\n\n        if(map.get(num)) {\n\n            int length = 1;\n\n            while(map.containsKey(num + length)) {\n                length++;\n            }\n\n            maxLength = Math.max(maxLength, length);\n        }\n    }\n\n    return maxLength;\n}\n<\/code><\/pre>\n\n<\/div>\n<p>}<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>\n\n\n---\n\n## Dry Run\n\n### Input\n\n\n\n```text id=\"h6vk5r\"\nnums = [100,4,200,1,3,2]\n<\/code><\/pre>\n\n<\/div>\n\n<h3>\n  \n  \n  Step 1: Mark Every Number as a Potential Starting Point\n<\/h3>\n\n\n\n<p>```text id=\"rm3f7s\"<br>\n100 -&gt; true<br>\n4   -&gt; true<br>\n200 -&gt; true<br>\n1   -&gt; true<br>\n3   -&gt; true<br>\n2   -&gt; true<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>\n\n\n### Step 2: Remove Numbers Having a Predecessor\n\nIf `(num - 1)` exists, it cannot be the beginning of a sequence.\n\n\n\n```text id=\"sl2w9v\"\n4 -&gt; false (3 exists)\n3 -&gt; false (2 exists)\n2 -&gt; false (1 exists)\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>Remaining starting points:<br>\n<\/p>\n\n<p>```text id=\"4xgn3w\"<br>\n100<br>\n200<br>\n1<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>\n\n\n### Step 3: Build Consecutive Sequences\n\n\n\n```text id=\"i6qtx8\"\n100 -&gt; length = 1\n\n200 -&gt; length = 1\n\n1 -&gt; 2 -&gt; 3 -&gt; 4\nlength = 4\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Answer\n<\/h3>\n\n\n\n<p>```text id=\"qvj2y1\"<br>\n4<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>\n\n\n---\n\n## Key Interview Takeaway\n\nThe trick is not finding consecutive numbers.\n\nThe trick is identifying only the valid sequence starting points and building sequences from there.\n\nThis simple observation reduces the solution from **O(N\u00b2)** to **O(N)** and is exactly what interviewers look for.\n\n---\n\nFollow along as I break down the intuition, brute force, better, and optimal solutions for every problem.\n\n**Master the starting-point pattern once, and many hashing problems become much easier to solve.**\n<\/code><\/pre>\n\n<\/div>\n\n","category":["algorithms","coding","computerscience","leetcode"]},{"title":"Building a Personal Study Planner Agent in typescript with HazelJS","pubDate":"Sun, 07 Jun 2026 18:54:46 +0000","link":"https:\/\/dev.to\/nisa_fatima_bcd75fa085b76\/building-a-personal-study-planner-agent-in-typescript-with-hazeljs-12g0","guid":"https:\/\/dev.to\/nisa_fatima_bcd75fa085b76\/building-a-personal-study-planner-agent-in-typescript-with-hazeljs-12g0","description":"<p>AI agents become more useful when they are not just chatbots, but small systems that can understand a request, call tools, retrieve knowledge, and return structured results.<\/p>\n\n<p>For this example, we built a <strong>Personal Study Planner Agent<\/strong> with HazelJS.<\/p>\n\n<p>The goal is simple:<\/p>\n\n<blockquote>\n<p>A student gives their exam date, available study time, and weak topics. The agent creates a realistic study plan.<\/p>\n<\/blockquote>\n\n<p>This is an easy project, but it still shows important HazelJS agent patterns.<\/p>\n\n<h2>\n  \n  \n  What the Project Includes\n<\/h2>\n\n<p>The project uses:<\/p>\n\n<ul>\n<li>\n<code>@hazeljs\/core<\/code> for modules, controllers, and services<\/li>\n<li>\n<code>@hazeljs\/agent<\/code> for agents, tools, delegation, and supervisor routing<\/li>\n<li>\n<code>@hazeljs\/rag<\/code> for study-method retrieval<\/li>\n<li>\n<code>@hazeljs\/guardrails<\/code> for safer input\/output handling<\/li>\n<li>\n<code>@hazeljs\/eval<\/code> for golden tests<\/li>\n<li>\n<code>@hazeljs\/inspector<\/code> for the <code>\/__hazel<\/code> web view<\/li>\n<\/ul>\n\n<p>The app has four agents:<\/p>\n\n<ul>\n<li><code>StudyIntakeAgent<\/code><\/li>\n<li><code>StudyResourceAgent<\/code><\/li>\n<li><code>StudyScheduleAgent<\/code><\/li>\n<li><code>StudyCoachAgent<\/code><\/li>\n<\/ul>\n\n<p>Each agent has a focused job.<\/p>\n\n<h2>\n  \n  \n  Why This Example Works Well\n<\/h2>\n\n<p>A study planner is simple enough to understand quickly, but it still has a real workflow:<\/p>\n\n<ol>\n<li>Extract study constraints<\/li>\n<li>Find useful study methods<\/li>\n<li>Build a schedule<\/li>\n<li>Return a clear plan<\/li>\n<\/ol>\n\n<p>That makes it a good beginner-friendly HazelJS agent example.<\/p>\n\n<p>A sample request looks like this:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>I have 10 days before my algebra exam. I can study 60 minutes daily. Weak topics are quadratic equations and word problems.\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>The system can extract the exam, timeline, daily capacity, weak topics, and urgency.<\/p>\n\n<h2>\n  \n  \n  Agent 1: Study Intake\n<\/h2>\n\n<p>The first agent is <code>StudyIntakeAgent<\/code>.<\/p>\n\n<p>Its job is to understand the student\u2019s request.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight typescript\"><code><span class=\"p\">@<\/span><span class=\"nd\">Agent<\/span><span class=\"p\">({<\/span>\n  <span class=\"na\">name<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">StudyIntakeAgent<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n  <span class=\"na\">description<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">Extracts exam goals, time constraints, weak topics, learning style, and urgency.<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n<span class=\"p\">})<\/span>\n<span class=\"p\">@<\/span><span class=\"nd\">Service<\/span><span class=\"p\">()<\/span>\n<span class=\"k\">export<\/span> <span class=\"kd\">class<\/span> <span class=\"nc\">StudyIntakeAgent<\/span> <span class=\"p\">{<\/span>\n  <span class=\"p\">@<\/span><span class=\"nd\">Tool<\/span><span class=\"p\">({<\/span>\n    <span class=\"na\">name<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">extractStudyProfile<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n    <span class=\"na\">description<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">Extract structured study planning information from a student request.<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n  <span class=\"p\">})<\/span>\n  <span class=\"k\">async<\/span> <span class=\"nf\">extractStudyProfile<\/span><span class=\"p\">(<\/span><span class=\"nx\">input<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span> <span class=\"nl\">message<\/span><span class=\"p\">:<\/span> <span class=\"kr\">string<\/span><span class=\"p\">;<\/span> <span class=\"nl\">studentId<\/span><span class=\"p\">?:<\/span> <span class=\"kr\">string<\/span> <span class=\"p\">})<\/span> <span class=\"p\">{<\/span>\n    <span class=\"c1\">\/\/ Extract exam, days, minutes, weak topics, and urgency<\/span>\n  <span class=\"p\">}<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>This keeps profile extraction separate from schedule creation.<\/p>\n\n<p>That is a good agent design pattern: one agent, one responsibility.<\/p>\n\n<h2>\n  \n  \n  Agent 2: Study Resources with RAG\n<\/h2>\n\n<p>The second agent is <code>StudyResourceAgent<\/code>.<\/p>\n\n<p>It uses <a href=\"https:\/\/hazeljs.ai\/\" rel=\"noopener noreferrer\">HazelJS<\/a> RAG to retrieve study methods like:<\/p>\n\n<ul>\n<li>active recall<\/li>\n<li>spaced repetition<\/li>\n<li>interleaving<\/li>\n<li>exam review<\/li>\n<li>healthy study breaks\n<\/li>\n<\/ul>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight typescript\"><code><span class=\"p\">@<\/span><span class=\"nd\">Agent<\/span><span class=\"p\">({<\/span>\n  <span class=\"na\">name<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">StudyResourceAgent<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n  <span class=\"na\">description<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">Retrieves evidence-based study methods and review tactics.<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n  <span class=\"na\">enableRAG<\/span><span class=\"p\">:<\/span> <span class=\"kc\">true<\/span><span class=\"p\">,<\/span>\n<span class=\"p\">})<\/span>\n<span class=\"p\">@<\/span><span class=\"nd\">Service<\/span><span class=\"p\">()<\/span>\n<span class=\"k\">export<\/span> <span class=\"kd\">class<\/span> <span class=\"nc\">StudyResourceAgent<\/span> <span class=\"p\">{<\/span>\n  <span class=\"p\">@<\/span><span class=\"nd\">Tool<\/span><span class=\"p\">({<\/span>\n    <span class=\"na\">name<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">searchStudyMethods<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n    <span class=\"na\">description<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">Search study method guidance for planning, review, recall, and exam preparation.<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n  <span class=\"p\">})<\/span>\n  <span class=\"k\">async<\/span> <span class=\"nf\">searchStudyMethods<\/span><span class=\"p\">(<\/span><span class=\"nx\">input<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span> <span class=\"nl\">query<\/span><span class=\"p\">:<\/span> <span class=\"kr\">string<\/span><span class=\"p\">;<\/span> <span class=\"nl\">topK<\/span><span class=\"p\">?:<\/span> <span class=\"kr\">number<\/span> <span class=\"p\">})<\/span> <span class=\"p\">{<\/span>\n    <span class=\"k\">return<\/span> <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">knowledgeBase<\/span><span class=\"p\">.<\/span><span class=\"nf\">answer<\/span><span class=\"p\">(<\/span><span class=\"nx\">input<\/span><span class=\"p\">.<\/span><span class=\"nx\">query<\/span><span class=\"p\">,<\/span> <span class=\"nx\">input<\/span><span class=\"p\">.<\/span><span class=\"nx\">topK<\/span> <span class=\"o\">??<\/span> <span class=\"mi\">3<\/span><span class=\"p\">);<\/span>\n  <span class=\"p\">}<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>This is better than asking the model to invent study advice. The agent retrieves known study methods and uses those as context.<\/p>\n\n<h2>\n  \n  \n  Agent 3: Study Schedule\n<\/h2>\n\n<p>The third agent is <code>StudyScheduleAgent<\/code>.<\/p>\n\n<p>It creates the actual day-by-day plan.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight typescript\"><code><span class=\"p\">@<\/span><span class=\"nd\">Agent<\/span><span class=\"p\">({<\/span>\n  <span class=\"na\">name<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">StudyScheduleAgent<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n  <span class=\"na\">description<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">Creates realistic daily study plans with review checkpoints.<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n<span class=\"p\">})<\/span>\n<span class=\"p\">@<\/span><span class=\"nd\">Service<\/span><span class=\"p\">()<\/span>\n<span class=\"k\">export<\/span> <span class=\"kd\">class<\/span> <span class=\"nc\">StudyScheduleAgent<\/span> <span class=\"p\">{<\/span>\n  <span class=\"p\">@<\/span><span class=\"nd\">Tool<\/span><span class=\"p\">({<\/span>\n    <span class=\"na\">name<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">createStudySchedule<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n    <span class=\"na\">description<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">Create a day-by-day study schedule from exam constraints and weak topics.<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n  <span class=\"p\">})<\/span>\n  <span class=\"k\">async<\/span> <span class=\"nf\">createStudySchedule<\/span><span class=\"p\">(<\/span><span class=\"nx\">input<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n    <span class=\"nl\">exam<\/span><span class=\"p\">:<\/span> <span class=\"kr\">string<\/span><span class=\"p\">;<\/span>\n    <span class=\"nl\">daysUntilExam<\/span><span class=\"p\">:<\/span> <span class=\"kr\">number<\/span><span class=\"p\">;<\/span>\n    <span class=\"nl\">minutesPerDay<\/span><span class=\"p\">:<\/span> <span class=\"kr\">number<\/span><span class=\"p\">;<\/span>\n    <span class=\"nl\">weakTopics<\/span><span class=\"p\">:<\/span> <span class=\"kr\">string<\/span><span class=\"p\">[];<\/span>\n  <span class=\"p\">})<\/span> <span class=\"p\">{<\/span>\n    <span class=\"c1\">\/\/ Build daily schedule<\/span>\n  <span class=\"p\">}<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>The tool returns a structured plan with:<\/p>\n\n<ul>\n<li>day number<\/li>\n<li>focus topic<\/li>\n<li>study minutes<\/li>\n<li>activities<\/li>\n<li>checkpoint<\/li>\n<\/ul>\n\n<p>This makes the output easier to use in a real app.<\/p>\n\n<h2>\n  \n  \n  Agent 4: Study Coach Orchestrator\n<\/h2>\n\n<p>The <code>StudyCoachAgent<\/code> coordinates the other agents.<\/p>\n\n<p>It uses HazelJS <code>@Delegate<\/code>:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight typescript\"><code><span class=\"p\">@<\/span><span class=\"nd\">Delegate<\/span><span class=\"p\">({<\/span>\n  <span class=\"na\">agent<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">StudyIntakeAgent<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n  <span class=\"na\">description<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">Extract exam, timeline, weak topics, capacity, and urgency.<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n  <span class=\"na\">inputField<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">input<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n<span class=\"p\">})<\/span>\n<span class=\"k\">async<\/span> <span class=\"nf\">analyzeStudyRequest<\/span><span class=\"p\">(<\/span><span class=\"nx\">input<\/span><span class=\"p\">:<\/span> <span class=\"kr\">string<\/span><span class=\"p\">):<\/span> <span class=\"nb\">Promise<\/span><span class=\"o\">&lt;<\/span><span class=\"kr\">string<\/span><span class=\"o\">&gt;<\/span> <span class=\"p\">{<\/span>\n  <span class=\"k\">return<\/span> <span class=\"dl\">''<\/span><span class=\"p\">;<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>It can delegate to:<\/p>\n\n<ul>\n<li><code>StudyIntakeAgent<\/code><\/li>\n<li><code>StudyResourceAgent<\/code><\/li>\n<li><code>StudyScheduleAgent<\/code><\/li>\n<\/ul>\n\n<p>This keeps orchestration clean without putting everything into one giant prompt.<\/p>\n\n<h2>\n  \n  \n  Supervisor Routing\n<\/h2>\n\n<p>The project also uses HazelJS supervisor routing.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight typescript\"><code><span class=\"kd\">const<\/span> <span class=\"nx\">supervisor<\/span> <span class=\"o\">=<\/span> <span class=\"nx\">runtime<\/span><span class=\"p\">.<\/span><span class=\"nf\">createSupervisor<\/span><span class=\"p\">({<\/span>\n  <span class=\"na\">name<\/span><span class=\"p\">:<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">study-planner-supervisor<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span>\n  <span class=\"na\">workers<\/span><span class=\"p\">:<\/span> <span class=\"p\">[<\/span><span class=\"dl\">'<\/span><span class=\"s1\">StudyIntakeAgent<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">StudyResourceAgent<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">StudyScheduleAgent<\/span><span class=\"dl\">'<\/span><span class=\"p\">],<\/span>\n  <span class=\"na\">maxRounds<\/span><span class=\"p\">:<\/span> <span class=\"mi\">4<\/span><span class=\"p\">,<\/span>\n<span class=\"p\">});<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>The supervisor decides which agent should handle the request.<\/p>\n\n<p>For example, a schedule request is routed to <code>StudyScheduleAgent<\/code>.<\/p>\n\n<p>A method question is routed to <code>StudyResourceAgent<\/code>.<\/p>\n\n<p>This is useful when the app does not know ahead of time which specialist should run.<\/p>\n\n<h2>\n  \n  \n  Runtime Safety and Observability\n<\/h2>\n\n<p>The app configures the HazelJS agent runtime with production-friendly settings:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight typescript\"><code><span class=\"nx\">AgentModule<\/span><span class=\"p\">.<\/span><span class=\"nf\">forRoot<\/span><span class=\"p\">({<\/span>\n  <span class=\"na\">runtime<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n    <span class=\"na\">defaultMaxSteps<\/span><span class=\"p\">:<\/span> <span class=\"mi\">8<\/span><span class=\"p\">,<\/span>\n    <span class=\"na\">defaultTimeout<\/span><span class=\"p\">:<\/span> <span class=\"mi\">15000<\/span><span class=\"p\">,<\/span>\n    <span class=\"na\">enableObservability<\/span><span class=\"p\">:<\/span> <span class=\"kc\">true<\/span><span class=\"p\">,<\/span>\n    <span class=\"na\">enableMetrics<\/span><span class=\"p\">:<\/span> <span class=\"kc\">true<\/span><span class=\"p\">,<\/span>\n    <span class=\"na\">enableRetry<\/span><span class=\"p\">:<\/span> <span class=\"kc\">true<\/span><span class=\"p\">,<\/span>\n    <span class=\"na\">enableCircuitBreaker<\/span><span class=\"p\">:<\/span> <span class=\"kc\">true<\/span><span class=\"p\">,<\/span>\n    <span class=\"na\">rateLimitPerMinute<\/span><span class=\"p\">:<\/span> <span class=\"mi\">120<\/span><span class=\"p\">,<\/span>\n  <span class=\"p\">},<\/span>\n<span class=\"p\">})<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>This gives the agent system:<\/p>\n\n<ul>\n<li>step limits<\/li>\n<li>timeouts<\/li>\n<li>retries<\/li>\n<li>circuit breaker protection<\/li>\n<li>metrics<\/li>\n<li>rate limiting<\/li>\n<li>observability<\/li>\n<\/ul>\n\n<p>Even for a small demo, these are good habits.<\/p>\n\n<h2>\n  \n  \n  Guardrails\n<\/h2>\n\n<p>The app enables HazelJS guardrails:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight typescript\"><code><span class=\"nx\">GuardrailsModule<\/span><span class=\"p\">.<\/span><span class=\"nf\">forRoot<\/span><span class=\"p\">({<\/span>\n  <span class=\"na\">redactPIIByDefault<\/span><span class=\"p\">:<\/span> <span class=\"kc\">true<\/span><span class=\"p\">,<\/span>\n  <span class=\"na\">blockInjectionByDefault<\/span><span class=\"p\">:<\/span> <span class=\"kc\">true<\/span><span class=\"p\">,<\/span>\n  <span class=\"na\">blockToxicityByDefault<\/span><span class=\"p\">:<\/span> <span class=\"kc\">true<\/span><span class=\"p\">,<\/span>\n<span class=\"p\">})<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>This helps keep inputs and outputs safer.<\/p>\n\n<p>For a study planner, guardrails are useful because users may paste personal details, stress-related messages, or unsafe instructions.<\/p>\n\n<h2>\n  \n  \n  Testing with Evals\n<\/h2>\n\n<p>The project includes golden evals with <code>@hazeljs\/eval<\/code>.<\/p>\n\n<p>The evals check:<\/p>\n\n<ul>\n<li>whether the intake agent calls <code>extractStudyProfile<\/code>\n<\/li>\n<li>whether the resource agent retrieves the right study methods<\/li>\n<li>whether the schedule agent calls <code>createStudySchedule<\/code>\n<\/li>\n<\/ul>\n\n<p>Example:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight json\"><code><span class=\"p\">{<\/span><span class=\"w\">\n  <\/span><span class=\"nl\">\"id\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">\"schedule-biology\"<\/span><span class=\"p\">,<\/span><span class=\"w\">\n  <\/span><span class=\"nl\">\"input\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">\"Build a plan for my biology exam in 14 days with 90 minutes daily. Weak topics are photosynthesis and genetics.\"<\/span><span class=\"p\">,<\/span><span class=\"w\">\n  <\/span><span class=\"nl\">\"expectedOutput\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">\"Study schedule complete\"<\/span><span class=\"p\">,<\/span><span class=\"w\">\n  <\/span><span class=\"nl\">\"expectedToolCalls\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"s2\">\"createStudySchedule\"<\/span><span class=\"p\">]<\/span><span class=\"w\">\n<\/span><span class=\"p\">}<\/span><span class=\"w\">\n<\/span><\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Run:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>npm run <span class=\"nb\">eval<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Expected result:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>[hazeljs\/eval] personal-study-planner-agent@2026.06 avg=0.944 passed=true\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>This proves the project is not only returning text, but also using the right tools.<\/p>\n\n<h2>\n  \n  \n  Running the Project\n<\/h2>\n\n<p>Install dependencies:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>npm <span class=\"nb\">install<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Run evals:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>npm run <span class=\"nb\">eval<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Start the app:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>npm run dev\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Test the supervisor endpoint:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>curl <span class=\"nt\">-s<\/span> <span class=\"nt\">-X<\/span> POST http:\/\/localhost:3000\/study\/supervisor <span class=\"se\">\\<\/span>\n  <span class=\"nt\">-H<\/span> <span class=\"s1\">'content-type: application\/json'<\/span> <span class=\"se\">\\<\/span>\n  <span class=\"nt\">-d<\/span> <span class=\"s1\">'{\"message\":\"I have 10 days before my algebra exam. I can study 60 minutes daily. Weak topics are quadratic equations and word problems.\",\"userId\":\"student-2\"}'<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Open the HazelJS inspector:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>http:\/\/localhost:3000\/__hazel\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>You can also inspect registered agents and tools:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>http:\/\/localhost:3000\/study\/runtime\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h2>\n  \n  \n  Why Use a Local Provider?\n<\/h2>\n\n<p>The demo uses a deterministic local provider.<\/p>\n\n<p>That means:<\/p>\n\n<ul>\n<li>no API key is required<\/li>\n<li>outputs are stable<\/li>\n<li>evals are repeatable<\/li>\n<li>readers can run the project immediately<\/li>\n<\/ul>\n\n<p>In production, you can replace it with a real model provider through <a href=\"https:\/\/hazeljs.ai\/\" rel=\"noopener noreferrer\">HazelJS<\/a> AI integrations.<\/p>\n\n<h2>\n  \n  \n  Final Thoughts\n<\/h2>\n\n<p>This project is intentionally simple, but it shows strong HazelJS agent patterns:<\/p>\n\n<ul>\n<li>focused agents<\/li>\n<li>scoped tools<\/li>\n<li>RAG-backed knowledge<\/li>\n<li>delegation<\/li>\n<li>supervisor routing<\/li>\n<li>guardrails<\/li>\n<li>runtime observability<\/li>\n<li>evals<\/li>\n<\/ul>\n\n<p>That makes the Personal Study Planner Agent a good beginner-friendly example for learning how to build practical AI agents with HazelJS.<br>\n<strong>Repo:<\/strong> <a href=\"https:\/\/github.com\/nisafatimaa\/personal-study-planner-agent\" rel=\"noopener noreferrer\">Personal Study Planner<\/a><\/p>\n\n","category":["agents","sideprojects","typescript","productivity"]},{"title":"The Business Impact of Technical Debt, Fragmented Architectures, and Legacy Systems","pubDate":"Sun, 07 Jun 2026 18:52:07 +0000","link":"https:\/\/dev.to\/denizceylan_kurt\/the-business-impact-of-technical-debt-fragmented-architectures-and-legacy-systems-h77","guid":"https:\/\/dev.to\/denizceylan_kurt\/the-business-impact-of-technical-debt-fragmented-architectures-and-legacy-systems-h77","description":"<p>Organizations often view technical debt as a technology problem. In reality, its impact extends far beyond software development teams. Technical debt, fragmented architectures, and legacy systems influence how quickly organizations can innovate, how efficiently teams can operate, and ultimately how effectively businesses can respond to changing market demands.<\/p>\n\n<p>Throughout my experience in enterprise environments, particularly within the insurance and financial services sector, I have observed that the biggest barriers to transformation are rarely new technologies. More often, the challenge lies within the complexity accumulated over years of system growth, business expansion, and short-term decision making.<\/p>\n\n<p>Enterprise systems are rarely built all at once. They evolve over time. New products are introduced, regulations change, customer expectations increase, and organizations adapt by adding new applications, integrations, and processes. While each individual change may solve an immediate business need, the cumulative effect can create an increasingly complex technology landscape.<\/p>\n\n<p>One of the most visible consequences of this complexity is slower delivery. Development teams frequently spend a significant portion of their time understanding existing systems before they can implement new functionality. Instead of focusing on innovation, they must first navigate undocumented processes, interconnected dependencies, and historical design decisions. As complexity grows, even relatively simple business changes can require extensive analysis and testing.<\/p>\n\n<p>Fragmented architectures create another challenge. In many organizations, business capabilities are distributed across multiple applications that were developed independently over different periods of time. Similar business rules may exist in several systems, often implemented in different ways. This duplication increases maintenance costs and introduces operational risk whenever changes are required. A single business requirement may need to be modified in multiple locations, increasing the likelihood of inconsistencies and defects.<\/p>\n\n<p>Data quality is equally affected by fragmented environments. Customer information, policy details, financial records, and operational data may be stored across numerous platforms with varying structures and standards. Over time, discrepancies emerge. Different systems may present different versions of the same information, making reporting, analytics, and decision-making more difficult. Organizations increasingly recognize that reliable data is not only important for operational efficiency but also essential for strategic initiatives such as artificial intelligence and advanced analytics.<\/p>\n\n<p>The growing interest in AI has brought renewed attention to these challenges. Many organizations are eager to adopt AI technologies, expecting significant improvements in productivity and customer experience. However, AI systems depend heavily on the quality of the data and processes that support them. Poorly integrated systems, inconsistent data models, and fragmented architectures can significantly limit the value that AI solutions are able to deliver.<\/p>\n\n<p>In many cases, the greatest obstacle to AI adoption is not the AI technology itself. It is the complexity of the underlying enterprise landscape. Organizations often discover that before they can fully leverage AI, they must first address foundational issues related to architecture, governance, standardization, and technical debt.<\/p>\n\n<p>This is why modernization should be viewed as a strategic business investment rather than a purely technical initiative. Successful modernization programs create long-term organizational value by reducing unnecessary complexity, improving system maintainability, strengthening data quality, and enabling faster delivery of future capabilities.<\/p>\n\n<p>Modernization does not necessarily mean replacing every legacy system. Instead, it involves making deliberate decisions about simplification, standardization, and architectural alignment. The objective is to create an environment where innovation becomes easier rather than harder over time.<\/p>\n\n<p>The organizations that succeed in digital transformation are often not those with the newest technologies, but those with the strongest foundations. By addressing technical debt, reducing fragmentation, and modernizing legacy environments, businesses position themselves to respond more effectively to future opportunities, whether those opportunities involve AI, automation, new products, or entirely new business models.<\/p>\n\n<p>Enterprise complexity grows naturally over time. Simplicity, however, must be designed intentionally. The organizations that understand this principle will be best prepared for the next generation of technological change.<\/p>\n\n","category":["ai","development","data","softwareengineering"]},{"title":"Git Worktrees: How to Run Multiple Claude Code Sessions at Once","pubDate":"Sun, 07 Jun 2026 18:51:12 +0000","link":"https:\/\/dev.to\/_suleyman\/git-worktrees-how-to-run-multiple-claude-code-sessions-at-once-1i81","guid":"https:\/\/dev.to\/_suleyman\/git-worktrees-how-to-run-multiple-claude-code-sessions-at-once-1i81","description":"<blockquote>\n<p>Git worktrees let you check out multiple branches at the same time, each in its own folder, all sharing one <code>.git<\/code> directory. And with Claude Code's <code>--worktree<\/code> flag, you can run multiple AI coding sessions at once without file conflicts. This guide covers everything from first setup to parallel AI workflows.<\/p>\n<\/blockquote>\n\n\n\n\n<p>You're working on a feature. You have uncommitted changes. A production bug comes in and needs a fix now.<\/p>\n\n<p>With standard Git, you have three bad options:<\/p>\n\n<ul>\n<li>\n<code>git stash<\/code> everything, switch branches, fix the bug, <code>git stash pop<\/code>, get back to where you were<\/li>\n<li>Clone the repo a second time and manage two out-of-sync copies<\/li>\n<li>Make a half-finished WIP commit just so you can switch branches<\/li>\n<\/ul>\n\n<p>Git worktrees remove all three of these options by letting you work on multiple branches at the same time, each in its own folder.<\/p>\n\n\n\n\n<h2>\n  \n  \n  What Are Git Worktrees?\n<\/h2>\n\n<p>A worktree is a folder with its own branch checked out. It shares the same <code>.git<\/code> history as your main project, but has its own working files.<\/p>\n\n<p>A normal clone gives you one folder, one branch at a time:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>~\/my-project\/\n  .git\/         \u2190 history lives here\n  src\/\n  package.json  \u2190 whatever branch you have checked out\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>With worktrees, you get multiple folders, each on its own branch:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>~\/my-project\/\n  .git\/              \u2190 one shared history\n  src\/\n  package.json       \u2190 main branch\n\n~\/my-project-auth\/\n  src\/\n  package.json       \u2190 feature\/auth branch\n\n~\/my-project-hotfix\/\n  src\/\n  package.json       \u2190 hotfix\/payment-crash branch\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>You can edit files in all three folders at the same time. Changes in one folder don't touch the others. And since they share one <code>.git<\/code>, a <code>git fetch<\/code> in any folder makes new remote branches visible everywhere.<\/p>\n\n<h3>\n  \n  \n  What's shared vs. what's separate\n<\/h3>\n\n<div class=\"table-wrapper-paragraph\"><table>\n<thead>\n<tr>\n<th>Shared across all worktrees<\/th>\n<th>Separate per worktree<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Commit history<\/td>\n<td>Checked-out branch<\/td>\n<\/tr>\n<tr>\n<td>Remote refs (after a fetch)<\/td>\n<td>Staged changes<\/td>\n<\/tr>\n<tr>\n<td>Hooks<\/td>\n<td>Unstaged changes<\/td>\n<\/tr>\n<tr>\n<td>Local Git config<\/td>\n<td>Working directory files<\/td>\n<\/tr>\n<tr>\n<td>Local branches<\/td>\n<td>Shell <code>$PWD<\/code>\n<\/td>\n<\/tr>\n<tr>\n<td>Tags<\/td>\n<td>\n<code>MERGE_HEAD<\/code>, <code>CHERRY_PICK_HEAD<\/code>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/div>\n\n\n\n\n<h2>\n  \n  \n  Worktrees vs. Multiple Clones\n<\/h2>\n\n<p>Some people solve the \"two branches at once\" problem by cloning the repo twice. Here's why that's worse:<\/p>\n\n<p><strong>Storage.<\/strong> Every clone copies the full <code>.git<\/code> object store. A 500 MB repo becomes 1 GB across two clones. Worktrees only add the weight of the working files \u2014 usually a few megabytes.<\/p>\n\n<p><strong>No shared state.<\/strong> <code>git fetch<\/code> in Clone A does nothing for Clone B. A local branch you create in Clone A doesn't exist in Clone B until you push it. You're managing two separate databases by hand.<\/p>\n\n<p><strong>Hooks only exist in one place.<\/strong> Git hooks (pre-commit linting, push guards, etc.) live in <code>.git\/hooks<\/code>. Clone B starts with nothing. With worktrees, all hooks apply everywhere because there's one <code>.git<\/code>.<\/p>\n\n<p><strong>Git protects you.<\/strong> Git won't let you check out the same branch in two worktrees at once. With two clones, nothing stops both from modifying the same branch simultaneously. You won't notice until you push.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Basic Setup: Your First Worktree\n<\/h2>\n\n<p>Start from a normal clone. These commands work from there.<\/p>\n\n<h3>\n  \n  \n  New worktree, new branch\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># Creates ..\/feature-auth directory on a new branch named feature\/auth<\/span>\ngit worktree add ..\/feature-auth <span class=\"nt\">-b<\/span> feature\/auth\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>The path and the branch name are independent. Use <code>-b<\/code> to name them separately.<\/p>\n\n<h3>\n  \n  \n  New worktree, auto-named branch\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># Branch name comes from the last part of the path: \"feature-auth\"<\/span>\ngit worktree add ..\/feature-auth\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Git names the branch after the last path segment. Fine for quick tasks. Use <code>-b<\/code> when the branch name matters.<\/p>\n\n<h3>\n  \n  \n  Worktree from an existing branch\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># Check out a branch that already exists<\/span>\ngit worktree add ..\/bugfix-session bugfix\/payment-crash\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Worktree from a remote branch\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># Create a local branch that tracks the remote one<\/span>\ngit worktree add <span class=\"nt\">-b<\/span> feature\/dark-mode ..\/dark-mode origin\/feature\/dark-mode\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Use this to review a colleague's branch without touching your own work.<\/p>\n\n<h3>\n  \n  \n  Then work normally\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"nb\">cd<\/span> ..\/feature-auth\nnpm <span class=\"nb\">install<\/span>       <span class=\"c\"># each worktree needs its own node_modules<\/span>\nnpm run dev\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Each worktree is a fresh checkout. Dependencies, virtual environments, and build output are not shared between worktrees. Install them in each one.<\/p>\n\n<h3>\n  \n  \n  List your worktrees\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git worktree list\n<\/code><\/pre>\n\n<\/div>\n\n\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>\/home\/you\/my-project        abc1234 [main]\n\/home\/you\/feature-auth      def5678 [feature\/auth]\n\/home\/you\/bugfix-session    ghi9012 [bugfix\/payment-crash]\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Remove a worktree\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git worktree remove ..\/feature-auth\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>This removes the folder and the Git bookkeeping. The branch stays unless you delete it with <code>git branch -d feature\/auth<\/code>.<\/p>\n\n\n\n\n<h2>\n  \n  \n  The Bare Clone Setup\n<\/h2>\n\n<p>The basic setup works, but the folder layout is awkward. Your main checkout sits alongside its own worktrees:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>~\/my-project\/    \u2190 main branch (also has working files)\n~\/feature-auth\/  \u2190 worktree\n~\/bugfix\/        \u2190 worktree\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>The main branch is in a special position even though it shouldn't be. Worktrees should all be equal.<\/p>\n\n<p>The bare clone setup fixes this. Instead of a normal clone, you clone only the history \u2014 no working files \u2014 and create every branch as a worktree, including <code>main<\/code>.<\/p>\n\n<h3>\n  \n  \n  How to set it up\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># 1. Create a clean project folder<\/span>\n<span class=\"nb\">mkdir<\/span> ~\/Projects\/my-project\n<span class=\"nb\">cd<\/span> ~\/Projects\/my-project\n\n<span class=\"c\"># 2. Clone history only, no working files<\/span>\ngit clone <span class=\"nt\">--bare<\/span> git@github.com:user\/repo.git .bare\n\n<span class=\"c\"># 3. Tell Git where the history is<\/span>\n<span class=\"nb\">echo<\/span> <span class=\"s2\">\"gitdir: .\/.bare\"<\/span> <span class=\"o\">&gt;<\/span> .git\n\n<span class=\"c\"># 4. Fix remote tracking \u2014 required, see explanation below<\/span>\ngit config remote.origin.fetch <span class=\"s2\">\"+refs\/heads\/*:refs\/remotes\/origin\/*\"<\/span>\n\n<span class=\"c\"># 5. Fetch all remote branches<\/span>\ngit fetch <span class=\"nt\">--all<\/span>\n\n<span class=\"c\"># 6. Create your first worktree<\/span>\ngit worktree add main\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Your folder now looks like this:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>~\/Projects\/my-project\/\n  .bare\/    \u2190 git history\n  .git      \u2190 one-line file pointing to .bare\n  main\/     \u2190 worktree for main branch\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Add more as needed:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git worktree add feature\/auth\ngit worktree add hotfix\/payment-crash\n<\/code><\/pre>\n\n<\/div>\n\n\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>~\/Projects\/my-project\/\n  .bare\/\n  .git\n  main\/\n  feature\/\n    auth\/\n  hotfix\/\n    payment-crash\/\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Every branch is a folder. When work is done and merged, remove the folder.<\/p>\n\n<h3>\n  \n  \n  The \"blind clone\" problem\n<\/h3>\n\n<p>After a bare clone, run <code>git fetch<\/code> and you might see only:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>* branch            HEAD       -&gt; FETCH_HEAD\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>That means Git only knows about the default branch. All other remote branches are invisible.<\/p>\n\n<p>This happens because bare clones are designed as server mirrors, not dev environments. They don't set up remote tracking by default.<\/p>\n\n<p><strong>Fix it:<\/strong><br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git config remote.origin.fetch <span class=\"s2\">\"+refs\/heads\/*:refs\/remotes\/origin\/*\"<\/span>\ngit fetch <span class=\"nt\">--all<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>The first line tells Git to map every remote branch to a local tracking reference. After that, <code>git fetch --all<\/code> shows everything. Run these two lines every time you do a bare clone.<\/p>\n\n<p>If you skip this step, <code>git worktree add<\/code> will silently create a new empty branch instead of checking out the remote branch you wanted.<\/p>\n\n<h3>\n  \n  \n  Automation script\n<\/h3>\n\n<p>Save this as <code>wtree<\/code> in your <code>~\/.local\/bin\/<\/code> and run <code>chmod +x wtree<\/code>:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\">#!\/bin\/bash<\/span>\n<span class=\"c\"># Usage: wtree &lt;git-url&gt;<\/span>\n<span class=\"c\"># Run this in an empty folder.<\/span>\n\n<span class=\"nv\">REPO_URL<\/span><span class=\"o\">=<\/span><span class=\"nv\">$1<\/span>\n<span class=\"nv\">SCRIPT_NAME<\/span><span class=\"o\">=<\/span><span class=\"si\">$(<\/span><span class=\"nb\">basename<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$0<\/span><span class=\"s2\">\"<\/span><span class=\"si\">)<\/span>\n\n<span class=\"k\">if<\/span> <span class=\"o\">[<\/span> <span class=\"nt\">-z<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$REPO_URL<\/span><span class=\"s2\">\"<\/span> <span class=\"o\">]<\/span><span class=\"p\">;<\/span> <span class=\"k\">then\n  <\/span><span class=\"nb\">echo<\/span> <span class=\"s2\">\"Usage: <\/span><span class=\"nv\">$0<\/span><span class=\"s2\"> &lt;repo-url&gt;\"<\/span>\n  <span class=\"nb\">exit <\/span>1\n<span class=\"k\">fi\n\n<\/span><span class=\"nv\">FILES<\/span><span class=\"o\">=<\/span><span class=\"si\">$(<\/span><span class=\"nb\">ls<\/span> <span class=\"nt\">-A<\/span> | <span class=\"nb\">grep<\/span> <span class=\"nt\">-v<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$SCRIPT_NAME<\/span><span class=\"s2\">\"<\/span><span class=\"si\">)<\/span>\n<span class=\"k\">if<\/span> <span class=\"o\">[<\/span> <span class=\"nt\">-n<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$FILES<\/span><span class=\"s2\">\"<\/span> <span class=\"o\">]<\/span><span class=\"p\">;<\/span> <span class=\"k\">then\n  <\/span><span class=\"nb\">echo<\/span> <span class=\"s2\">\"Error: folder is not empty.\"<\/span>\n  <span class=\"nb\">exit <\/span>1\n<span class=\"k\">fi\n\n<\/span>git clone <span class=\"nt\">--bare<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$REPO_URL<\/span><span class=\"s2\">\"<\/span> .bare\n<span class=\"nb\">echo<\/span> <span class=\"s2\">\"gitdir: .\/.bare\"<\/span> <span class=\"o\">&gt;<\/span> .git\ngit config remote.origin.fetch <span class=\"s2\">\"+refs\/heads\/*:refs\/remotes\/origin\/*\"<\/span>\ngit fetch <span class=\"nt\">--all<\/span>\n\n<span class=\"nb\">echo<\/span> <span class=\"s2\">\"Done. Run: git worktree add main\"<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n\n\n\n<h2>\n  \n  \n  Command Reference\n<\/h2>\n\n<h3>\n  \n  \n  <code>git worktree add<\/code>\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># New branch, explicit name<\/span>\ngit worktree add &lt;path&gt; <span class=\"nt\">-b<\/span> &lt;branch&gt;\n\n<span class=\"c\"># New branch, name comes from path<\/span>\ngit worktree add &lt;path&gt;\n\n<span class=\"c\"># Existing local branch<\/span>\ngit worktree add &lt;path&gt; &lt;existing-branch&gt;\n\n<span class=\"c\"># From a remote branch<\/span>\ngit worktree add <span class=\"nt\">-b<\/span> &lt;local-name&gt; &lt;path&gt; &lt;remote&gt;\/&lt;branch&gt;\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  <code>git worktree list<\/code>\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git worktree list\n\n<span class=\"c\"># Machine-readable output<\/span>\ngit worktree list <span class=\"nt\">--porcelain<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  <code>git worktree remove<\/code>\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># Remove a worktree (fails if there are uncommitted changes)<\/span>\ngit worktree remove &lt;path&gt;\n\n<span class=\"c\"># Force removal, discards uncommitted changes<\/span>\ngit worktree remove <span class=\"nt\">--force<\/span> &lt;path&gt;\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  <code>git worktree prune<\/code>\n<\/h3>\n\n<p>If you delete a worktree folder with <code>rm -rf<\/code> instead of <code>git worktree remove<\/code>, Git keeps stale records pointing to the now-missing folder. <code>prune<\/code> cleans those up.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># Remove stale records<\/span>\ngit worktree prune\n\n<span class=\"c\"># Preview what would be removed<\/span>\ngit worktree prune <span class=\"nt\">--dry-run<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  <code>git worktree lock<\/code> \/ <code>unlock<\/code>\n<\/h3>\n\n<p>Prevent <code>prune<\/code> from removing a worktree \u2014 useful for worktrees on external drives:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git worktree lock &lt;path&gt; <span class=\"nt\">--reason<\/span> <span class=\"s2\">\"On external drive\"<\/span>\ngit worktree unlock &lt;path&gt;\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  <code>git worktree move<\/code>\n<\/h3>\n\n<p>Move a worktree to a new path:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git worktree move ..\/feature-auth ..\/workspaces\/feature-auth\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  <code>git worktree repair<\/code>\n<\/h3>\n\n<p>If you moved the <code>.git<\/code> directory or the worktree folder manually, Git loses track of it. Run this from inside the worktree to re-link it:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git worktree repair\n<\/code><\/pre>\n\n<\/div>\n\n\n\n\n\n\n<h2>\n  \n  \n  Real-World Workflows\n<\/h2>\n\n<h3>\n  \n  \n  Hotfix while in the middle of a feature\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># You're in ~\/project\/feature\/redesign with uncommitted work.<\/span>\n<span class=\"c\"># A bug needs fixing now. Don't stash, don't stop.<\/span>\n\n<span class=\"c\"># From the project root, add a worktree for the fix<\/span>\ngit worktree add hotfix\/payment-crash\n\n<span class=\"c\"># Open a second terminal<\/span>\n<span class=\"nb\">cd<\/span> ~\/project\/hotfix\/payment-crash\n<span class=\"c\"># Fix the bug<\/span>\ngit add <span class=\"nb\">.<\/span>\ngit commit <span class=\"nt\">-m<\/span> <span class=\"s2\">\"fix: null check on payment processor callback\"<\/span>\ngit push origin hotfix\/payment-crash\n\n<span class=\"c\"># Your first terminal still has the redesign exactly where you left it<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Compare two implementations side by side\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git worktree add approach-a <span class=\"nt\">-b<\/span> experiment\/approach-a\ngit worktree add approach-b <span class=\"nt\">-b<\/span> experiment\/approach-b\n\n<span class=\"c\"># Two terminals, two dev servers<\/span>\n<span class=\"nb\">cd <\/span>approach-a <span class=\"o\">&amp;&amp;<\/span> npm run dev <span class=\"nt\">--<\/span> <span class=\"nt\">--port<\/span> 3001\n<span class=\"nb\">cd <\/span>approach-b <span class=\"o\">&amp;&amp;<\/span> npm run dev <span class=\"nt\">--<\/span> <span class=\"nt\">--port<\/span> 3002\n\n<span class=\"c\"># Open both in the browser and compare<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Review a PR without losing your place\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git fetch origin pull\/1234\/head:pr-1234\ngit worktree add ..\/pr-1234 pr-1234\n\n<span class=\"nb\">cd<\/span> ..\/pr-1234\n<span class=\"c\"># Review, run tests, check behavior<\/span>\n\n<span class=\"c\"># When done<\/span>\n<span class=\"nb\">cd<\/span> ..\ngit worktree remove pr-1234\ngit branch <span class=\"nt\">-d<\/span> pr-1234\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Long-running parallel features\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git worktree add feature\/payments <span class=\"nt\">-b<\/span> feature\/payments\ngit worktree add feature\/notifications <span class=\"nt\">-b<\/span> feature\/notifications\n\n<span class=\"c\"># Work on payments in one terminal, notifications in another.<\/span>\n<span class=\"c\"># git fetch in either one updates both.<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n\n\n\n<h2>\n  \n  \n  Shell Aliases and Shortcuts\n<\/h2>\n\n<h3>\n  \n  \n  Git aliases\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git config <span class=\"nt\">--global<\/span> alias.wta <span class=\"s1\">'!f() { git worktree add -b \"$1\" \"..\/$1\"; }; f'<\/span>\ngit config <span class=\"nt\">--global<\/span> alias.wtr <span class=\"s1\">'!f() { git worktree remove \"..\/$1\"; }; f'<\/span>\ngit config <span class=\"nt\">--global<\/span> alias.wtl <span class=\"s1\">'worktree list'<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Usage:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git wta feature-auth    <span class=\"c\"># creates ..\/feature-auth on branch feature-auth<\/span>\ngit wtr feature-auth    <span class=\"c\"># removes it<\/span>\ngit wtl                 <span class=\"c\"># lists all worktrees<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  PR checkout function\n<\/h3>\n\n<p>Add to your <code>~\/.bashrc<\/code> or <code>~\/.zshrc<\/code>. Requires the <a href=\"https:\/\/cli.github.com\/\" rel=\"noopener noreferrer\">GitHub CLI<\/a>:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># Usage: cpr &lt;PR_NUMBER&gt;<\/span>\ncpr<span class=\"o\">()<\/span> <span class=\"o\">{<\/span>\n  <span class=\"nb\">local pr<\/span><span class=\"o\">=<\/span><span class=\"s2\">\"<\/span><span class=\"nv\">$1<\/span><span class=\"s2\">\"<\/span>\n  <span class=\"nb\">local <\/span><span class=\"nv\">remote<\/span><span class=\"o\">=<\/span><span class=\"s2\">\"<\/span><span class=\"k\">${<\/span><span class=\"nv\">2<\/span><span class=\"k\">:-<\/span><span class=\"nv\">origin<\/span><span class=\"k\">}<\/span><span class=\"s2\">\"<\/span>\n  <span class=\"nb\">local <\/span>branch\n  <span class=\"nv\">branch<\/span><span class=\"o\">=<\/span><span class=\"si\">$(<\/span>gh <span class=\"nb\">pr <\/span>view <span class=\"s2\">\"<\/span><span class=\"nv\">$pr<\/span><span class=\"s2\">\"<\/span> <span class=\"nt\">--json<\/span> headRefName <span class=\"nt\">-q<\/span> .headRefName<span class=\"si\">)<\/span>\n  git fetch <span class=\"s2\">\"<\/span><span class=\"nv\">$remote<\/span><span class=\"s2\">\"<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$branch<\/span><span class=\"s2\">\"<\/span>\n  git worktree add <span class=\"s2\">\"..\/<\/span><span class=\"nv\">$branch<\/span><span class=\"s2\">\"<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$branch<\/span><span class=\"s2\">\"<\/span>\n  <span class=\"nb\">cd<\/span> <span class=\"s2\">\"..\/<\/span><span class=\"nv\">$branch<\/span><span class=\"s2\">\"<\/span> <span class=\"o\">||<\/span> <span class=\"k\">return\n  <\/span><span class=\"nb\">echo<\/span> <span class=\"s2\">\"Ready: PR #<\/span><span class=\"nv\">$pr<\/span><span class=\"s2\"> (<\/span><span class=\"nv\">$branch<\/span><span class=\"s2\">)\"<\/span>\n<span class=\"o\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Run <code>cpr 1234<\/code> and you're in a folder with that PR checked out.<\/p>\n\n<h3>\n  \n  \n  Fuzzy worktree switcher\n<\/h3>\n\n<p>Requires <a href=\"https:\/\/github.com\/junegunn\/fzf\" rel=\"noopener noreferrer\"><code>fzf<\/code><\/a>:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>wts<span class=\"o\">()<\/span> <span class=\"o\">{<\/span>\n  <span class=\"nb\">local <\/span>selected\n  <span class=\"nv\">selected<\/span><span class=\"o\">=<\/span><span class=\"si\">$(<\/span>git worktree list | <span class=\"nb\">awk<\/span> <span class=\"s1\">'{print $1}'<\/span> | fzf <span class=\"nt\">--prompt<\/span><span class=\"o\">=<\/span><span class=\"s2\">\"Switch to worktree: \"<\/span><span class=\"si\">)<\/span>\n  <span class=\"o\">[<\/span> <span class=\"nt\">-n<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$selected<\/span><span class=\"s2\">\"<\/span> <span class=\"o\">]<\/span> <span class=\"o\">&amp;&amp;<\/span> <span class=\"nb\">cd<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$selected<\/span><span class=\"s2\">\"<\/span>\n<span class=\"o\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Type <code>wts<\/code>, search by name, press Enter.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Editor Tooling\n<\/h2>\n\n<h3>\n  \n  \n  VS Code, Cursor, Windsurf\n<\/h3>\n\n<p>Install the <a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=PhilStainer.git-worktree\" rel=\"noopener noreferrer\">Git Worktree extension<\/a> by PhilStainer. It adds worktree commands to the Command Palette:<\/p>\n\n<ul>\n<li>List and jump between worktrees<\/li>\n<li>Add and remove worktrees from the GUI<\/li>\n<\/ul>\n\n<p>Each worktree opens as a separate VS Code window since each is a different folder on disk. Treat each one as its own project.<\/p>\n\n<p>Pairs well with the <a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=alefragnani.project-manager\" rel=\"noopener noreferrer\">Project Manager extension<\/a> if you switch between many worktrees often.<\/p>\n\n<h3>\n  \n  \n  Neovim \/ Vim\n<\/h3>\n\n<p>Session managers like <a href=\"https:\/\/github.com\/tpope\/vim-obsession\" rel=\"noopener noreferrer\">vim-obsession<\/a> or <a href=\"https:\/\/github.com\/folke\/persistence.nvim\" rel=\"noopener noreferrer\">persistence.nvim<\/a> save a session per directory automatically, so each worktree gets its own saved state.<\/p>\n\n<h3>\n  \n  \n  JetBrains IDEs\n<\/h3>\n\n<p>Open each worktree as a separate project window. The IDE detects the shared <code>.git<\/code> and shows branch info correctly.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Claude Code + Worktrees\n<\/h2>\n\n<p>Without worktrees, two Claude Code sessions in the same directory will overwrite each other's files. The second session has no idea what the first wrote. You get silent conflicts.<\/p>\n\n<p>Worktrees give each session its own directory and branch. They can run at the same time without touching each other's work.<\/p>\n\n<p>Claude Code has this built in with the <code>--worktree<\/code> flag.<\/p>\n\n<h3>\n  \n  \n  Start a session in a worktree\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>claude <span class=\"nt\">--worktree<\/span> feature-auth\n<span class=\"c\"># short form:<\/span>\nclaude <span class=\"nt\">-w<\/span> bugfix-payment\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Claude creates <code>.claude\/worktrees\/feature-auth\/<\/code>, checks out a new branch named <code>worktree-feature-auth<\/code>, and starts the session there.<\/p>\n\n<p>Open another terminal and run a second session:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>claude <span class=\"nt\">-w<\/span> bugfix-payment\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Both run at the same time. Neither can see the other's files.<\/p>\n\n<p><strong>First-time setup:<\/strong> Before using <code>--worktree<\/code> in a repo, run <code>claude<\/code> once in that directory to accept the workspace trust prompt. After that, the flag works without extra steps.<\/p>\n\n<h3>\n  \n  \n  Let Claude pick a name\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>claude <span class=\"nt\">--worktree<\/span>\n<span class=\"c\"># Creates something like: .claude\/worktrees\/bright-running-fox\/<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Check out a pull request\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>claude <span class=\"nt\">--worktree<\/span> <span class=\"s2\">\"#1234\"<\/span>\n<span class=\"c\"># Creates .claude\/worktrees\/pr-1234\/ from that PR's branch<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Claude fetches the branch and starts the session in it.<\/p>\n\n<h3>\n  \n  \n  Copy <code>.env<\/code> files into every worktree\n<\/h3>\n\n<p>Worktrees are fresh checkouts. Files like <code>.env<\/code> and <code>.env.local<\/code> won't be there unless you copy them.<\/p>\n\n<p>Create a <code>.worktreeinclude<\/code> file in the project root. Same syntax as <code>.gitignore<\/code>. Only files that match AND are already gitignored will be copied:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight conf\"><code><span class=\"c\"># .worktreeinclude\n<\/span>.<span class=\"n\">env<\/span>\n.<span class=\"n\">env<\/span>.<span class=\"n\">local<\/span>\n<span class=\"n\">config<\/span>\/<span class=\"n\">secrets<\/span>.<span class=\"n\">json<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>This applies to every worktree Claude creates \u2014 via <code>--worktree<\/code>, via subagents, or via the desktop app.<\/p>\n\n<p>Also add this to <code>.gitignore<\/code> so worktree contents don't show up as untracked files:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>.claude\/worktrees\/\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Isolate subagents\n<\/h3>\n\n<p>When Claude delegates work to subagents, multiple subagents can write to the same files at once \u2014 same conflict problem. Tell Claude to isolate them:<\/p>\n\n<blockquote>\n<p>\"Use worktrees for your agents.\"<\/p>\n<\/blockquote>\n\n<p>Or set it permanently in a custom subagent's frontmatter:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight yaml\"><code><span class=\"nn\">---<\/span>\n<span class=\"na\">name<\/span><span class=\"pi\">:<\/span> <span class=\"s\">backend-agent<\/span>\n<span class=\"na\">isolation<\/span><span class=\"pi\">:<\/span> <span class=\"s\">worktree<\/span>\n<span class=\"nn\">---<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Each subagent gets its own worktree. When the subagent finishes with no changes, the worktree is removed automatically.<\/p>\n\n<h3>\n  \n  \n  Base branch: remote vs. local HEAD\n<\/h3>\n\n<p>By default, new worktrees branch from <code>origin\/HEAD<\/code> \u2014 the latest remote state. To branch from your current local <code>HEAD<\/code> instead (including unpushed commits), add this to your Claude Code settings:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight json\"><code><span class=\"p\">{<\/span><span class=\"w\">\n  <\/span><span class=\"nl\">\"worktree\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span><span class=\"w\">\n    <\/span><span class=\"nl\">\"baseRef\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">\"head\"<\/span><span class=\"w\">\n  <\/span><span class=\"p\">}<\/span><span class=\"w\">\n<\/span><span class=\"p\">}<\/span><span class=\"w\">\n<\/span><\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Use <code>\"head\"<\/code> when subagents need to work on code you haven't pushed yet. Use the default when you want each session to start from a clean remote state.<\/p>\n\n<h3>\n  \n  \n  What happens when a session ends\n<\/h3>\n\n<div class=\"table-wrapper-paragraph\"><table>\n<thead>\n<tr>\n<th>Session state on exit<\/th>\n<th>What Claude does<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>No commits, no changes<\/td>\n<td>Removes the worktree and branch automatically<\/td>\n<\/tr>\n<tr>\n<td>Has commits or uncommitted changes<\/td>\n<td>Asks: keep or remove?<\/td>\n<\/tr>\n<tr>\n<td>Non-interactive mode (<code>-p<\/code> flag)<\/td>\n<td>No cleanup \u2014 remove manually<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/div>\n\n<p>To clean up manually:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git worktree list\ngit worktree remove .claude\/worktrees\/feature-auth\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Desktop app\n<\/h3>\n\n<p>The Claude Code desktop app creates a worktree for every new parallel session automatically. No flags needed. <code>.worktreeinclude<\/code> still applies.<\/p>\n\n<h3>\n  \n  \n  Three sessions at once: a full example\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># .gitignore<\/span>\n<span class=\"nb\">echo<\/span> <span class=\"s2\">\".claude\/worktrees\/\"<\/span> <span class=\"o\">&gt;&gt;<\/span> .gitignore\n\n<span class=\"c\"># .worktreeinclude<\/span>\n<span class=\"nb\">echo<\/span> <span class=\"s2\">\".env\"<\/span> <span class=\"o\">&gt;&gt;<\/span> .worktreeinclude\n\n<span class=\"c\"># Three terminals<\/span>\nclaude <span class=\"nt\">-w<\/span> add-oauth-login\nclaude <span class=\"nt\">-w<\/span> fix-rate-limiter\nclaude <span class=\"nt\">-w<\/span> migrate-to-drizzle\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>When each finishes:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># Review the work<\/span>\ngit diff main..worktree-add-oauth-login\ngit diff main..worktree-fix-rate-limiter\ngit diff main..worktree-migrate-to-drizzle\n\n<span class=\"c\"># Merge what's good<\/span>\ngit merge worktree-fix-rate-limiter\n<\/code><\/pre>\n\n<\/div>\n\n\n\n\n\n\n<h2>\n  \n  \n  Common Pitfalls\n<\/h2>\n\n<h3>\n  \n  \n  Deleting a folder with <code>rm -rf<\/code> instead of <code>git worktree remove<\/code>\n<\/h3>\n\n<p>Git keeps records pointing to the deleted folder. The next time you try to add a worktree at the same path:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>fatal: 'feature-auth' is already checked out at '...'\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Fix:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git worktree prune\ngit worktree add &lt;path&gt; &lt;branch&gt;\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  You can't check out the same branch in two worktrees\n<\/h3>\n\n<p>Git blocks this. If you need two sessions on the same branch, create a new branch from it:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git worktree add ..\/review-copy <span class=\"nt\">-b<\/span> review\/feature-auth feature\/auth\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Dependencies aren't shared\n<\/h3>\n\n<p><code>node_modules<\/code>, Python virtual environments, compiled binaries \u2014 each worktree needs its own. After creating a worktree:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"nb\">cd<\/span> ..\/new-worktree\nnpm <span class=\"nb\">install<\/span>   <span class=\"c\"># or pip install, cargo build, etc.<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Bare clone without the fetch fix\n<\/h3>\n\n<p>After <code>git clone --bare<\/code>, always run:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git config remote.origin.fetch <span class=\"s2\">\"+refs\/heads\/*:refs\/remotes\/origin\/*\"<\/span>\ngit fetch <span class=\"nt\">--all<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Skip this and you can only see the default branch. Everything else on the remote is invisible.<\/p>\n\n<h3>\n  \n  \n  Running many sessions in parallel is resource-heavy\n<\/h3>\n\n<p>Multiple Claude sessions each running tests, compilation, and a dev server adds up. Four parallel sessions runs fine on an M-series Mac with 16 GB RAM. Adjust based on your hardware.<\/p>\n\n<h3>\n  \n  \n  <code>claude --worktree<\/code> fails with a workspace trust error\n<\/h3>\n\n<p>Run <code>claude<\/code> once in the project folder to accept the trust prompt. Then <code>--worktree<\/code> works:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>claude      <span class=\"c\"># accept the prompt, then Ctrl+C<\/span>\nclaude <span class=\"nt\">-w<\/span> my-task   <span class=\"c\"># now works<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n\n\n\n<h2>\n  \n  \n  Cheat Sheet\n<\/h2>\n\n<h3>\n  \n  \n  Git commands\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># Create worktree (new branch)<\/span>\ngit worktree add &lt;path&gt; <span class=\"nt\">-b<\/span> &lt;branch&gt;\n\n<span class=\"c\"># Create worktree (existing branch)<\/span>\ngit worktree add &lt;path&gt; &lt;existing-branch&gt;\n\n<span class=\"c\"># Create worktree (from remote branch)<\/span>\ngit worktree add <span class=\"nt\">-b<\/span> &lt;name&gt; &lt;path&gt; &lt;remote&gt;\/&lt;branch&gt;\n\n<span class=\"c\"># List all worktrees<\/span>\ngit worktree list\n\n<span class=\"c\"># Remove a worktree<\/span>\ngit worktree remove &lt;path&gt;\n\n<span class=\"c\"># Clean up stale worktree records<\/span>\ngit worktree prune\n\n<span class=\"c\"># Lock a worktree (protect from prune)<\/span>\ngit worktree lock &lt;path&gt;\n\n<span class=\"c\"># Move a worktree<\/span>\ngit worktree move &lt;old-path&gt; &lt;new-path&gt;\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Git aliases\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>git config <span class=\"nt\">--global<\/span> alias.wta <span class=\"s1\">'!f() { git worktree add -b \"$1\" \"..\/$1\"; }; f'<\/span>\ngit config <span class=\"nt\">--global<\/span> alias.wtr <span class=\"s1\">'!f() { git worktree remove \"..\/$1\"; }; f'<\/span>\ngit config <span class=\"nt\">--global<\/span> alias.wtl <span class=\"s1\">'worktree list'<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Bare clone setup\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"nb\">mkdir<\/span> ~\/Projects\/my-project <span class=\"o\">&amp;&amp;<\/span> <span class=\"nb\">cd<\/span> ~\/Projects\/my-project\ngit clone <span class=\"nt\">--bare<\/span> &lt;repo-url&gt; .bare\n<span class=\"nb\">echo<\/span> <span class=\"s2\">\"gitdir: .\/.bare\"<\/span> <span class=\"o\">&gt;<\/span> .git\ngit config remote.origin.fetch <span class=\"s2\">\"+refs\/heads\/*:refs\/remotes\/origin\/*\"<\/span>\ngit fetch <span class=\"nt\">--all<\/span>\ngit worktree add main\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Claude Code\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># Named worktree<\/span>\nclaude <span class=\"nt\">--worktree<\/span> &lt;name&gt;\nclaude <span class=\"nt\">-w<\/span> &lt;name&gt;\n\n<span class=\"c\"># Auto-named<\/span>\nclaude <span class=\"nt\">--worktree<\/span>\n\n<span class=\"c\"># From a pull request<\/span>\nclaude <span class=\"nt\">--worktree<\/span> <span class=\"s2\">\"#&lt;pr-number&gt;\"<\/span>\n\n<span class=\"c\"># settings.json \u2014 branch from local HEAD<\/span>\n<span class=\"o\">{<\/span> <span class=\"s2\">\"worktree\"<\/span>: <span class=\"o\">{<\/span> <span class=\"s2\">\"baseRef\"<\/span>: <span class=\"s2\">\"head\"<\/span> <span class=\"o\">}<\/span> <span class=\"o\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Files to create in your project\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"c\"># .gitignore<\/span>\n<span class=\"nb\">echo<\/span> <span class=\"s2\">\".claude\/worktrees\/\"<\/span> <span class=\"o\">&gt;&gt;<\/span> .gitignore\n\n<span class=\"c\"># .worktreeinclude<\/span>\n<span class=\"nb\">echo<\/span> <span class=\"s2\">\".env\"<\/span> <span class=\"o\">&gt;&gt;<\/span> .worktreeinclude\n<span class=\"nb\">echo<\/span> <span class=\"s2\">\".env.local\"<\/span> <span class=\"o\">&gt;&gt;<\/span> .worktreeinclude\n<\/code><\/pre>\n\n<\/div>\n\n\n\n\n\n\n<h2>\n  \n  \n  Summary\n<\/h2>\n\n<p>Worktrees change one thing: instead of having one branch checked out at a time, every branch is a folder. Switching context means opening a different terminal, not stashing and hoping you remember what you were doing.<\/p>\n\n<p>The bare clone setup takes five minutes to learn and gives you a cleaner structure for repos where you work across many branches regularly.<\/p>\n\n<p>If you use Claude Code, the <code>--worktree<\/code> flag is the practical way to run multiple sessions without them stepping on each other. Set up <code>.worktreeinclude<\/code> once, add <code>.claude\/worktrees\/<\/code> to <code>.gitignore<\/code>, and you're done.<\/p>\n\n","category":["claude","git","tooling","productivity"]},{"title":"How I Lost a Six-Figure SaaS Contract to a \"Vibe Coder\" (Even Though I Used AI Too)","pubDate":"Sun, 07 Jun 2026 18:45:18 +0000","link":"https:\/\/dev.to\/vincent_phiri_fc220a95092\/how-i-lost-a-six-figure-saas-contract-to-a-vibe-coder-even-though-i-used-ai-too-1411","guid":"https:\/\/dev.to\/vincent_phiri_fc220a95092\/how-i-lost-a-six-figure-saas-contract-to-a-vibe-coder-even-though-i-used-ai-too-1411","description":"<p><em>Have you seen the think-pieces. \"AI is coming for your jobs.\" \"English is the new programming language.\" As a seasoned software developer based in South Africa, I used to think I was ahead of the curve. I wasn't an AI denier; in fact, I actively use Agentic AI assistants to cut down my development time, spin up boilerplates, and ship features faster than ever.<\/em><\/p>\n\n<p>I thought leveraging AI inside my IDE made me very competitive.<\/p>\n\n<p>Oh boy was i wrong by huge margin. Recently, I didn't just lose a lucrative, six-figure administrative SaaS contract to AI\u2014I lost it to a human who didn't write a single line of code. I lost it to an Operations Manager who decided to \"vibe code\" an entire alternative system.<\/p>\n\n<p>Here is the wake-up call of how it happened, the sobering conversation we had, and why merely using AI as a coding assistant does not put us(software developers) head and shoulders above the rest.<\/p>\n\n<h2>\n  \n  \n  The Setup: Fast Shipping, False Security\n<\/h2>\n\n<p>In early 2025, I landed a highly competitive contract to build a comprehensive administrative SaaS solution for a prominent local company in South Africa which does high end corporate events and venue management services. There was no single source of truth, operations and admin very disjointed. <\/p>\n\n<p>They need a solution to streamline operations and have one source of truth to make operations as efficient and effective as possible.I did my workflows audits and came up with a workable system that the owners loved, well eventually. There were feedback meetings we held after my presentation and made adjustments from the suggestions I got from management and lower staff. After a month or so my solution was adopted and went live. It was a bespoke SaaS.<\/p>\n\n<p>Using Claude Code helped me code components, generate APIs, and I rapidly deployed both a web portal and mobile application. Thanks to AI assistance, I built it in record time. The client was thrilled, and the project naturally transitioned into a highly lucrative monthly maintenance contract.<\/p>\n\n<p>I felt completely secure. I was the \"10x engineer\" who knew how to prompt, how to code, and how to architect. I held the keys to the repository.<\/p>\n\n<p>Then came the Operations Manager.<\/p>\n\n<h2>\n  \n  \n  The Plot Twist: \"Please don't be angry with me...\"\n<\/h2>\n\n<p>The company\u2019s Operations Manager is brilliant at his job, but he has zero software development background. He couldn't write a loop if his life depended on it. What he did have, however, was a 100% granular view of the company's daily operational bottlenecks\u2014angles I hadn't fully captured because I didn't live in their day-to-day chaos.<br>\nInstead of waiting for me to build custom features on my retainer, he signed up to Claude Max5 began vibe coding. He just fed the AI business logic, real-world data structures, and company pain points.<\/p>\n\n<p>Within two months, he didn't just build a prototype. He built a comprehensive, highly practical, fully functioning alternative SaaS solution.<br>\nWhen he finally revealed what he had done, he asked to sit down with me. I'll never forget how the conversation started. He looked at me nervously and said:<\/p>\n\n<blockquote>\n<p>Please don\u2019t be angry with me. What I\u2019m about to tell you might make you furiously angry with me...<br>\nHe then laid out his alternative system and explained his reasoning plainly: <\/p>\n\n<p>I didn\u2019t want the company to keep spending massive amounts of money on a maintenance retainer when I could just build what we needed myself. It took me two months, spent about $700 on the build.<\/p>\n<\/blockquote>\n\n<p>His solution was vastly cheaper than my contract, completely tailored to their exact workflows, and it worked. He successfully beat me out of my own contract.<\/p>\n\n<h2>\n  \n  \n  The Realization: AI-Assisted Coding vs. Pure Vibe Coding\n<\/h2>\n\n<p>This was a massive slice of humble pie. My immediate defense mechanism was to think, <\/p>\n\n<blockquote>\n<p>But I used AI to build my system too! My code is cleaner, safer, and better architected!<\/p>\n<\/blockquote>\n\n<p>But here is the brutal reality we have to face as professional developers:<strong>Knowing how to code using AI didn't protect me from the guy who doesn't know how to code at all.<\/strong><\/p>\n\n<p>Why? Because I was using AI to write code. He was using AI to write solutions.<\/p>\n\n<p>I was focused on syntax efficiency, database optimization, and deployment pipelines. He was focused 100% on the organizational problems because he understood the business inside and out. <br>\nTo management, a $700 system built by an insider that solves 95% of their immediate operational problems today will always win over a beautifully architected, expensive system built by an external dev, who is me and you.<\/p>\n\n<p>By being dismissive or elitist about \"vibe coders,\" we are actively pushing ourselves out of business. Our direct competition is no longer just other software agencies; it\u2019s the ambitious, frustrated employee inside our client's office.<\/p>\n\n<h2>\n  \n  \n  The Blueprint: How We Take the Lead\n<\/h2>\n\n<p>If someone with zero coding knowledge can build a super-alternative SaaS system in two months for a few hundred dollars, imagine what we can do if we combine actual engineering fundamentals with that same level of business-centric execution.<\/p>\n\n<p><em>We need to shift our focus immediately:<\/em><br>\n<strong>1. Stop Hiding Behind the Code<\/strong><br>\nHaving our hands 100% under the hood isn't the flex it used to be. We need to spend less time worrying about the lines of code and more time understanding the business domain. If we don\u2019t have a 100% view of the organization\u2019s actual day-to-day problems, a vibe coder who does will replace us.<\/p>\n\n<p><strong>2. Move from Developers to \"Super-Architects\"<\/strong><br>\nVibe-coded applications are amazing for immediate business logic, but they eventually hit walls regarding complex security, data compliance (like POPIA here in South Africa), and massive scaling. Our value is no longer in typing the code faster than them; it's in being the guardrails. We should be the ones helping them architect, secure, and scale these systems safely.<\/p>\n\n<p><strong>3. Build at 100x Speed, Not 10x<\/strong><br>\nIf non-technical builders are using AI to disrupt us, we must use our technical knowledge to out-pace them. We know how databases should handle edge cases. We know how APIs should be secured. By combining deep engineering logic with high-speed AI generation, a single professional developer should be able to ship entire enterprise ecosystems over a weekend.<\/p>\n\n<h2>\n  \n  \n  The Silverlining\n<\/h2>\n\n<p>Losing that contract was a painful lesson, but it forced me to see where the tech landscape is heading. The gatekeeping era of software development is officially dead.<\/p>\n\n<p>Have you ever been bypassed by a non-technical stakeholder building their own tools? If you are a developer who uses AI, how are you changing your strategy to ensure you're delivering unique value that a \"vibe coder\" can't replicate?<\/p>\n\n","category":["ai","vibecoding","softwaredevelopment"]},{"title":"We stopped Googling and started Prompting","pubDate":"Sun, 07 Jun 2026 18:44:55 +0000","link":"https:\/\/dev.to\/dinall\/we-stopped-googling-and-started-prompting-3i83","guid":"https:\/\/dev.to\/dinall\/we-stopped-googling-and-started-prompting-3i83","description":"<p>An analysis of how search behavior is shifting from traditional search engines to LLM-driven prompting, and what it means for SEO, content strategy, and EEAT.<\/p>\n\n<h2>\n  \n  \n  The greatest behavioral shift of the decade happened without anyone noticing.\n<\/h2>\n\n<p>For more than two decades, digital behavior was stable.<\/p>\n\n<p>If you needed information, you opened Google, typed a query, scanned a list of links, and manually assembled an answer from multiple sources.<\/p>\n\n<p>That model shaped SEO, content marketing, and how knowledge on the internet was structured.<\/p>\n\n<p>But something fundamental has changed \u2014 quietly and without a clear breaking point.<\/p>\n\n<p>People are no longer primarily searching.<\/p>\n\n<p>They are prompting.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Search was retrieval. Prompting is synthesis.\n<\/h2>\n\n<p>Traditional search engines are built around retrieval. You input keywords, and you receive documents ranked by relevance signals like backlinks, freshness, and authority.<\/p>\n\n<p>Large language models invert this model. Instead of returning sources, they return answers. Instead of presenting documents, they synthesize them.<\/p>\n\n<p>This is not just a UX improvement. It changes the unit of value on the internet.<\/p>\n\n<p>Previously, the goal was to rank a page.<br>\nNow, the goal is to be included in a generated answer.<\/p>\n\n\n\n\n<h2>\n  \n  \n  What this breaks in SEO\n<\/h2>\n\n<p>Classic SEO assumes a predictable flow:<\/p>\n\n<p>query \u2192 SERP \u2192 click \u2192 page \u2192 conversion or engagement<\/p>\n\n<p>That chain is now optional.<\/p>\n\n<p>In many cases, the user never reaches a page at all. The answer is generated directly inside the interface.<\/p>\n\n<p>This reduces the importance of:<\/p>\n\n<ul>\n<li>ranking position as the primary success metric\n<\/li>\n<li>click-through optimization as the main bottleneck\n<\/li>\n<li>keyword targeting as the central strategy\n<\/li>\n<\/ul>\n\n<p>And increases the importance of something less obvious:<\/p>\n\n<p>how easily your content can be understood, extracted, and reused by a model.<\/p>\n\n\n\n\n<h2>\n  \n  \n  The new layer: LLM-oriented visibility\n<\/h2>\n\n<p>We are still calling it SEO, but the mechanics are shifting.<br>\nInstead of optimizing only for search engines, content now has to be legible to language models.<\/p>\n\n<p>That means:<\/p>\n\n<p>Content needs to be structurally clear, fact-dense, and unambiguous.<br>\nNot because humans cannot understand complexity, but because models prioritize patterns they can reliably compress into answers.<\/p>\n\n<p>In practice, this favors:<\/p>\n\n<ul>\n<li>direct definitions instead of indirect storytelling\n<\/li>\n<li>consistent terminology across pages and domains\n<\/li>\n<li>explicit relationships between concepts\n<\/li>\n<li>minimal ambiguity in claims and descriptions\n<\/li>\n<\/ul>\n\n<p>Authority is no longer just about backlinks. It is about how consistently your information appears across the broader data ecosystem.<\/p>\n\n\n\n\n<h2>\n  \n  \n  EEAT becomes more important, not less\n<\/h2>\n\n<p>Google\u2019s EEAT framework (Experience, Expertise, Authoritativeness, Trustworthiness) was already important for search ranking.<\/p>\n\n<p>In an LLM-driven environment, it becomes even more relevant, but in a different way.<\/p>\n\n<p>Models are trained on patterns of consensus and repetition across authoritative sources.<\/p>\n\n<p>Content that demonstrates real expertise and is aligned with widely trusted information has a higher chance of being represented correctly in generated answers.<\/p>\n\n<p>So EEAT does not disappear \u2014 it becomes structural.<\/p>\n\n<ul>\n<li>Experience shows up as depth and specificity\n<\/li>\n<li>Expertise shows up as correctness and precision\n<\/li>\n<li>Authority shows up as consistency across sources\n<\/li>\n<li>Trust shows up as lack of contradictions and noise\n<\/li>\n<\/ul>\n\n\n\n\n<h2>\n  \n  \n  From traffic optimization to representation optimization\n<\/h2>\n\n<p>The key mental shift is this:<br>\nYou are no longer optimizing only for visitors.<br>\nYou are optimizing for representation inside generated knowledge.<br>\nThat means your content can \u201cwin\u201d even without direct clicks, if it becomes part of the system that produces answers.<br>\nThis creates a new category of content strategy:<\/p>\n\n<p>not just SEO pages, but answer-ready knowledge.<\/p>\n\n\n\n\n<h2>\n  \n  \n  What content starts to look like in this model\n<\/h2>\n\n<p>Content that performs well in LLM-driven environments tends to:<\/p>\n\n<ul>\n<li>answer specific questions directly and early\n<\/li>\n<li>avoid unnecessary narrative expansion before value is delivered\n<\/li>\n<li>structure information in clearly separable ideas\n<\/li>\n<li>maintain consistent terminology across topics\n<\/li>\n<li>prioritize factual density over stylistic variation\n<\/li>\n<\/ul>\n\n<p>This does not mean content becomes dry. It means content becomes modular.<\/p>\n\n<p>Think less like storytelling, and more like building blocks of knowledge.<\/p>\n\n\n\n\n<h2>\n  \n  \n  A quiet shift in distribution\n<\/h2>\n\n<p>Another change is happening at the same time: distribution itself is moving into AI interfaces.<\/p>\n\n<p>Users are no longer only discovering content through search engines or social feeds. They are discovering it through conversations with models.<\/p>\n\n<p>This creates a second-order effect:<\/p>\n\n<p>Content that is easy for models to interpret becomes more likely to be surfaced, summarized, or referenced inside those conversations.<\/p>\n\n\n\n\n<h2>\n  \n  \n  A small but practical example\n<\/h2>\n\n<p>As content production scales, formatting and packaging also become part of the system.<\/p>\n\n<p>For example, blog posts increasingly need associated visual assets that are generated automatically rather than manually designed.<\/p>\n\n<p>Tools like <a href=\"https:\/\/thumbapi.dev\" rel=\"noopener noreferrer\">ThumbAPI<\/a> can be used to generate consistent blog and social thumbnails directly from article titles, reducing manual design work and standardizing visual output across large content pipelines.<\/p>\n\n<p>This is not about design tooling in isolation, but about infrastructure for content distribution in an AI-mediated ecosystem.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Conclusion\n<\/h2>\n\n<p>The shift from Googling to Prompting is not a trend in interfaces.<br>\nIt is a change in how information flows through the internet.<br>\nSearch engines indexed pages.Language models synthesize knowledge.<br>\nAnd in that transition, the most important change is simple:<br>\nVisibility is no longer about being clicked.<br>\nIt is about being included in the answer.<\/p>\n\n","category":["seo","llm","ai","contentwriting"]},{"title":"Is Vibe Coding Over? The Free Lunch Is Ending, Not the Movement","pubDate":"Sun, 07 Jun 2026 18:43:47 +0000","link":"https:\/\/dev.to\/mmar58\/is-vibe-coding-over-the-free-lunch-is-ending-not-the-movement-3ak2","guid":"https:\/\/dev.to\/mmar58\/is-vibe-coding-over-the-free-lunch-is-ending-not-the-movement-3ak2","description":"<p>Over the last four years, software development has gone through one of the fastest transformations in its history. We moved from searching for answers, to chatting with AI, to delegating entire features to autonomous agents. What started as a productivity boost evolved into what many now call <strong>vibe coding<\/strong>: describing what you want in plain text and letting AI figure out the implementation.<\/p>\n\n<p>But in 2026, a new reality is emerging.<\/p>\n\n<ul>\n<li>The technology keeps getting better.<\/li>\n<li>The economics are getting harder.<\/li>\n<\/ul>\n\n<p>The question is no longer whether AI can write code. The question is whether we can afford to use it the way we've become accustomed to.<\/p>\n\n\n\n\n<h2>\n  \n  \n  From Search to Chat\n<\/h2>\n\n<p>Before late 2022, solving a technical problem usually meant opening Google, reading Stack Overflow threads, digging through documentation, and stitching together a solution manually. The process worked, but it was slow.<\/p>\n\n<p>Then conversational AI arrived. Instead of searching through ten links and adapting answers ourselves, we could simply describe the problem and receive a tailored solution instantly. Follow-up questions turned coding from a search problem into a continuous conversation. The workflow fundamentally changed: developers shifted from search-first development to chat-first development.<\/p>\n\n\n\n\n<h2>\n  \n  \n  From Chat to Vibe Coding\n<\/h2>\n\n<p>The next leap was agentic development. AI stopped being just an assistant that answered standalone questions. It started reading entire repositories, creating files, fixing bugs, writing tests, and implementing features across complex projects.<\/p>\n\n<p>The workflow became: <strong>\"Build this feature,\"<\/strong> instead of <strong>\"Write this function.\"<\/strong><\/p>\n\n<p>Early agents made plenty of mistakes, but the models improved quickly. For startups and small teams, the productivity gains were impossible to ignore. A single engineer acting as an instructor could use a $20 to $50 agent setup to match the output of a small team. Naturally, vibe coding exploded.<\/p>\n\n\n\n\n<h2>\n  \n  \n  The Hidden Subsidy Meets Token Economics\n<\/h2>\n\n<p>The early economics looked almost magical, but there was a catch. Many AI providers were heavily subsidizing usage to acquire customers and gain market share. The apparent cost of AI was far lower than the actual cost of running compute. As providers move toward sustainable business models, those subsidies are rapidly disappearing.<\/p>\n\n<p>The clearest example is the industry's shift toward strict usage-based token billing. Instead of paying a flat monthly fee and treating AI as effectively unlimited, teams are increasingly billed based on actual token consumption.<\/p>\n\n<p>The price difference in daily workflows is stark:<\/p>\n\n<div class=\"table-wrapper-paragraph\"><table>\n<thead>\n<tr>\n<th>AI Era \/ Pricing Model<\/th>\n<th>Workflow Scope<\/th>\n<th>Approximate Cost<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>\n<strong>Flat-Rate Subscription<\/strong> (Subsidized)<\/td>\n<td>Building\/iterating multiple apps roughly over a day<\/td>\n<td>\n<strong>$1 \u2013 $3<\/strong> total<\/td>\n<\/tr>\n<tr>\n<td>\n<strong>Usage-Based Agentic Model<\/strong> (Current)<\/td>\n<td>Deep, multi-platform system review or codebase-wide analysis<\/td>\n<td>\n<strong>$1<\/strong> per 2\u20133 minutes<\/td>\n<\/tr>\n<tr>\n<td>\n<strong>Complex Autonomous Loops<\/strong> (Current)<\/td>\n<td>Endless agentic debugging across a large repository<\/td>\n<td>\n<strong>$100 \u2013 $300<\/strong> per session<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/div>\n\n<blockquote>\n<p><strong>The Reality Check:<\/strong> A project that previously cost around $14 of flat-rate usage can easily snowball into a $100 to $300 bill if an autonomous agent is allowed to loop endlessly on a complex system.<\/p>\n<\/blockquote>\n\n<p>The conversation is shifting from <em>\"Can AI do this?\"<\/em> to <em>\"Is AI the most cost-effective way to do this?\"<\/em> AI is no longer a free utility; it is a resource that must be budgeted and managed like cloud infrastructure, compute, or API usage.<\/p>\n\n\n\n\n<h2>\n  \n  \n  The Pragmatic Future\n<\/h2>\n\n<p>Vibe coding isn't ending; it is maturing. The first phase of AI development encouraged developers to blindly delegate everything possible. The next phase will be far more strategic.<\/p>\n\n<p>Instead of asking agents to run wild on infinite loops, technical managers and architects will plan and sanction limited budgets for specific areas. We will deploy them where the return on investment is highest:<\/p>\n\n<ul>\n<li>Boilerplate generation and manual typing savings<\/li>\n<li>Isolating and fixing repetitive bugs<\/li>\n<li>Writing comprehensive test suites<\/li>\n<li>Reviewing code and documentation<\/li>\n<\/ul>\n\n<p>Developers will spend less time typing and more time planning, validating, and making high-level architectural decisions. The role shifts from a code-writer to a system engineer.<\/p>\n\n\n\n\n<h2>\n  \n  \n  The Rise of Local AI\n<\/h2>\n\n<p>At the same time, the hardware landscape is adapting to these economic pressures. New systems, such as NVIDIA's <strong>RTX Spark<\/strong> platform, are making it entirely realistic to run massive open-weight models locally on a single workstation.<\/p>\n\n<p>For many organizations, shifting the day-to-day contextual heavy lifting to local inference will become significantly cheaper than repeatedly paying cloud token fees. The future will likely be a hybrid framework:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight plaintext\"><code>                  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                  \u2502   Software Development Task  \u2502\n                  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                 \u2502\n                 Is it a complex reasoning\/\n                 architectural problem?\n                  \/              \\\n               YES                NO\n               \/                    \\\n  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n  \u2502   Cloud-Based LLM     \u2502      \u2502    Local LLM Setup    \u2502\n  \u2502 (High-End Reasoning)  \u2502      \u2502 (RTX Spark \/ Routine) \u2502\n  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>The winning teams will be the ones who optimize their pipelines for both capability and cost.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Conclusion\n<\/h2>\n\n<p>Vibe coding is not over. The wave of low-effort \"vibe coders\" who don't understand the underlying systems will likely fade as the financial costs rise. But AI-assisted development is a permanent fixture of software engineering.<\/p>\n\n<p>The biggest change isn't technical; it's economic. Success will no longer come from blindly delegating everything to an agent, nor from refusing to use AI at all. The most effective developers will be those who understand both engineering and economics\u2014knowing when AI creates leverage, when human judgment is required, and how to balance capability against cost.<\/p>\n\n<p>The free lunch is over. The AI era is not.<\/p>\n\n","category":["vibecoding","ai","softwaredevelopment","llm"]}]}}