NoteRunway: Because Your Notion Workspace Deserves an Elite Crew

Main Page

This is a submission for the Notion MCP Challenge

Table of Contents

  • What I Built

    • Explanatory Video
    • Introduction
    • The Backstory
    • The 7 Tools
  • Show us the code
  • How I Used Notion MCP

    • The Architecture
    • Why MCP and Not Just the API?
    • Destructive Tool Safety
  • The Tech Behind It

    • Multi-Provider AI (BYOK)
    • The Archive System
  • Wrapping Up

What I Built

Explanatory Video

Introduction

NoteRunway: an AI-powered workspace management tool that helps Notion power users clean up, audit, and command their workspace through a single interface.

Think of it as a flight control tower for your Notion workspace. It scans for problems you didn’t know you had, visualizes connections you couldn’t see, and lets you talk to your workspace in plain English.

🔗 Live Demo → noterunway.pilotronica.com

Main Page

The Backstory

I’m a Notion addict. Not the “I use it for grocery lists” kind, rather the “I have 400+ pages, nested databases inside databases, and a set of half-written ideas from 2 AM” kind.

It all started in 2020, when I decided it was time to bring more order into my life. My notes were scattered everywhere, there was some structure, but not nearly enough. I needed something more powerful… something more organized.

Then I discovered Notion and instantly knew it was exactly what I’d been looking for all along and didn’t know existed.

You know how it goes, at first, you’re fully motivated. Everything’s clean, structured, flawless. You’re convinced this time you’ll manage your notes perfectly.

But sooner or later, every power user ends up in the same place: orphaned pages from long-forgotten projects, duplicate notes written three times because the first two vanished into the void, and dead links leading to pages you archived months ago.

And somewhere in that chaos, there’s probably a note with an API key you definitely didn’t mean to paste and leave it forever. Have you ever pasted an API key or secret thinking “I’m gonna move it somewhere more secure later”? And that “later” doesn’t really come, does it?

I needed a tool that could see the bigger picture, something that could scan everything, something that’d surface what’s broken, and help me fix it without blindly deleting content or forcing me to manually comb through bajillion pages.

When the Notion MCP Challenge dropped, it clicked right away. I could build that tool, and give something back to the Notion community while I’m at it.

The 7 Tools

NoteRunway packs 7 workspace management tools, split between pure scanners and AI-powered analyzers.

Think of it as a crew of seven tools at your disposal, everything you need to keep your Notion workspace in top shape. Each one has a specific role: some scan your workspace to surface hidden issues, others go deeper to analyze patterns and give you smarter insights.

Together, they cut through the clutter, highlight what actually needs attention, and help you clean things up without the risk of losing something important. No guesswork, no endless manual checks, just a clear view of your workspace and the tools to keep it running smoothly.

📊 Workspace Health Dashboard

Dashboard

Your workspace at a glance. Total pages, top-level pages, recently edited, empty pages, and link density (how well-connected your workspace actually is).

It’s the “oh wow, I have how many empty pages?” moment.

🔍 Duplicate Detection

Duplicate Detection

This is where the AI earns its keep. NoteRunway fetches up to 100 pages with their content snippets, sends them to the LLM with a specialized system prompt, and gets back semantic duplicate groups with similarity scores.

Not just “same title” duplicates, it catches pages with different names but overlapping content. The AI picks a recommended “keep” version (⭐), and you choose which ones to archive.

🗑️ Garbage Collector

Garbage Collector

Rule-based detection across three categories:

  • Orphaned — parent page was deleted, child is floating in limbo
  • Empty — zero content blocks (you created it and… forgot)
  • Stale — untouched for 90+ days (configurable)

Dry-run mode is on by default. You review everything before anything moves.

🔗 Dead Link Detector

Dead Link Detector

Finds @mentions pointing to pages that no longer exist. Every broken mention is tracked back to its source page, so you know exactly where to fix it.

🔐 Sensitive Data Finder

Sensitive Data Finder

Two-phase security scanner:

Phase 1 (Regex): Scans all pages against 13 patterns: API keys (OpenAI, Anthropic, Stripe, AWS, GitHub), PEM keys, JWTs, database URLs, passwords, credit cards. Findings are always partially redacted, never exposing full values.

Phase 2 (AI Deep Scan): Optionally sends page text to the LLM to catch natural-language secrets that regex misses. Things like “the password is hunter2” or “login: admin/secret123”.

🕸️ Dependency Graph

Dependency Graph

An interactive force-directed graph of your entire workspace. Pages are nodes (colored by depth), connections are edges (solid for parent-child, dashed for @mentions). Orphaned pages glow red.

You can hover, click, collapse branches, and zoom around. It’s surprisingly therapeutic to see your workspace as a living network.

💬 Semantic Ask (Agentic Chat)

Semantic Ask

A natural language interface to your workspace, powered by an agentic AI loop.

Type something like “Find all pages about authentication and summarize them” or “Archive everything in the old-project folder.” The AI breaks it into tool calls (search, read, analyze), feeds results back into its context, and proposes structured actions for your approval.

It supports 5 action types: archive, create, rename, append, and update, all with a human-in-the-loop confirmation step. No action executes without your explicit “yes.”

Just a heads-up: be cautious with destructive actions. Avoid updating or archiving critical data.

This project is still in beta, and I don’t want you to risk losing important information. As you know, AI isn’t deterministic, and unexpected outcomes can happen.

Show us the code

Here’s the repository:

NoteRunway

AI-Powered Notion Workspace Management

Clean, organize, and command your Notion workspace with AI. Powered by the Notion MCP.

NoteRunway

🔗 Live Demo → noterunway.pilotronica.com

Next.js
TypeScript
Tailwind CSS
Notion MCP
Vercel AI SDK
License

Table of Contents

Overview

NoteRunway is a browser-based tool that helps Notion power users clean up, audit, and manage their workspace using a combination of deterministic scans and AI-powered intelligence via the Notion MCP.

It connects to your Notion workspace through OAuth, runs server-side analysis via Next.js API routes, and provides a cyberpunk-themed UI for reviewing findings and approving actions.

Core Principles

Principle Description
Zero Lock-in Your data stays in Notion. NoteRunway never stores workspace content.
Human-in-the-Loop No destructive action runs without explicit user approval.
Privacy First AI keys stored only in your browser (BYOK). Sent per-request to NoteRunway APIs and never persisted server-side.

…

How I Used Notion MCP

This is the core of the whole project. NoteRunway uses the Notion MCP Server as the execution layer for all workspace interactions in the Semantic Ask feature.

The Architecture

Browser → Next.js API (SSE) → Vercel AI SDK → MCP Client → notion-mcp-server (stdio) → Notion API

Here’s the flow:

  1. User sends a message in the Ask interface
  2. Vercel AI SDK processes it with streamText() and the selected LLM
  3. When the AI decides it needs workspace data, it calls tools: search_pages, get_page, get_page_content, run_analysis
  4. These tools route through MCPClient, which communicates with the Notion MCP server over stdio (JSON-RPC 2.0)
  5. The MCP server translates tool calls into Notion API requests and returns structured results
  6. Results feed back into the AI’s context for the next reasoning step (up to 10 steps)
  7. When the AI wants to make changes, it calls propose_actions, which streams proposed actions to the UI for human approval
  8. Only after the user confirms does the action execute via MCP

Why MCP and Not Just the API?

I use both. Here’s the split:

  • NotionClient (direct SDK) for bulk reads, workspace scans, pagination-heavy operations. It’s faster for fetching 400 pages than routing through MCP.
  • MCPClient (MCP server) for all writes like creating, archiving, renaming, updating pages. MCP provides a sandboxed execution layer with tool-level safety.

This hybrid approach gives me the speed of direct API access with the safety of MCP’s tool calling protocol.

Destructive Tool Safety

Any tool that modifies workspace data (patch, post-page, delete, move) requires an approved: true flag. The AI can propose actions, but nothing destructive runs without the user clicking “Execute.”

The Tech Behind It

Layer Technology Why
Framework Next.js 16 (App Router) Full-stack React with API routes
AI SDK Vercel AI SDK 6 Unified streaming + tool calling across 4 providers
MCP @notionhq/notion-mcp-server Notion workspace access via Model Context Protocol
Visualization React Flow 11 Force-directed graph for the dependency viewer
Validation Zod 4 Runtime schema validation for all API inputs/outputs
Styling Tailwind CSS 4 + shadcn/ui Cyberpunk aesthetic with accessible components

Multi-Provider AI (BYOK)

NoteRunway supports 4 AI providers with 13 models: OpenAI (GPT-5.4, GPT-5 Mini), Anthropic (Claude Opus 4.6, Haiku 4.5), xAI (Grok 4, Grok 3), and Google (Gemini 2.5 Pro/Flash).

Your API key stays in your browser’s localStorage. It’s sent per-request in an HTTP header, used once to create a provider instance, and discarded. NoteRunway’s server never stores, logs, or persists your key.

The Archive System

Every destructive action creates an audit trail:

NoteRunway Archive/
├── Duplicates/
│   └── [Audit Stub] Original Title
├── Garbage Collection/
│   └── [Audit Stub] Stale Page Name
└── Semantic Ask/
    └── [Audit Stub] Archived via Chat

Each audit stub contains a callout with the archive date, reason, and a faithful copy of the original content. The original page is soft-archived to Notion’s trash (recoverable for 30 days).

Nothing is ever truly deleted without a paper trail.

Wrapping Up

NoteRunway started as a personal itch, a messy workspace and a dev challenge deadline. It turned into something I’m going to use every week.

Building with the Notion MCP was a revelation. The ability to give an AI structured access to a workspace, with tool-level control over what it can read and write, opens up workflows that feel like magic. Ask a question, get an answer sourced from your actual pages. Propose a cleanup, review it, execute it. All in one flow.

If you’re a Notion power user drowning in pages, give it a spin: noterunway.pilotronica.com

And if you find an API key you forgot about in a meeting note… you’re welcome. 😄

Built with ☕ and too many late nights by Giorgi Kobaidze

I built a payment processing platform in Mexico with FastAPI — here’s what I learned after 10+ production deployments

I’ve been running FastAPI in production for a payment processing platform in Mexico for the past couple of years. We handle SPEI transfers (Mexico’s interbank transfer system), OXXO Pay (cash payments at convenience stores), and crypto on-ramps through multiple PSP integrations.
I wanted to share some real lessons from production that I don’t see discussed much.
The stack

FastAPI + async SQLAlchemy 2.0 + asyncpg
PostgreSQL + Redis (rate limiting + caching)
Docker → AWS ECS Fargate
GitHub Actions CI/CD
Alembic for migrations

Lesson 1: Webhook state machines save your life
When you process payments, you receive webhooks from payment providers. The naive approach is to just update the payment status directly. Don’t do this.
We built a webhook state machine where each payment goes through defined states: pending → processing → completed/failed. Every state transition is logged with timestamps. When a payment provider sends duplicate webhooks (and they will), the state machine just ignores transitions that don’t make sense.
This saved us during a reconciliation where we found timezone boundary issues between UTC and CST that were causing mismatches at midnight.
Lesson 2: Alembic migrations + ECS = restart loops
This one hurt. We had an Alembic migration that was taking too long, and ECS health checks were killing the container before the migration finished. Container restarts, migration starts over, health check fails again. Infinite loop.
Fix: Run migrations as a separate ECS task before deploying the new container. Never run migrations on startup in production.
Lesson 3: psycopg2 + LIKE queries will bite you
If you use % in LIKE queries with psycopg2 and parameterized queries, you need %% for literal percent signs when mixed with %s placeholders. We had a bug where a search endpoint was silently returning empty results.

Wrong

cursor.execute(“SELECT * FROM users WHERE name LIKE ‘%s%'”, (search,))

Right

cursor.execute(“SELECT * FROM users WHERE name LIKE %s”, (f”%{search}%”,))
Lesson 4: VARCHAR timestamps are a trap
One of our PSP integrations stores created_at as VARCHAR instead of TIMESTAMP. Every time we need to do date comparisons, we have to CAST(created_at AS TIMESTAMP). If you’re designing your schema, always use proper TIMESTAMP columns.
Lesson 5: Rate limiting is not optional
We got hit with a brute force attack on our auth endpoints within the first month. Redis-backed rate limiting with SlowAPI saved us. 5 attempts per minute on login, 20 requests per minute on general endpoints.
Lesson 6: Connection pooling matters more than you think
Our KYC service was falling over under load. The fix? Increasing the connection pool from 10 to 50 connections. That’s it. Went from random 500 errors to zero issues.
The architecture pattern that works
After iterating through multiple services, this is the pattern I settled on:
Repository (data access)
→ Service (business logic)
→ Endpoint (HTTP layer)
Each layer only talks to the one below it. Dependencies are injected via FastAPI’s Depends(). Testing becomes trivial — just mock the repository layer.

After refining this over 10+ deployments, I packaged the boilerplate into a starter kit with the full auth system, async DB, Docker, CI/CD, and tests. It’s on my Gumroad if anyone wants it. But the lessons above are the real value — the code is just the implementation.
I also deploy production AWS infrastructure for teams that don’t want to deal with Terraform/ECS — details here if that’s useful.
Happy to answer questions about running FastAPI in production, dealing with Mexican payment providers, or anything else.

Demystifying REST APIs: A Comprehensive Guide from Beginner to Architect

If you’ve spent more than a week in web development, you’ve encountered REST APIs. They are the glue of the modern internet, connecting front-end frameworks to back-end databases and enabling third-party integrations that we take for granted.

But there is a massive difference between “making an API call” and “understanding RESTful architecture.” Today, we’re going deep. We’ll start with the basics and move into the advanced patterns that separate junior developers from senior architects.

Want to dive deeper into full-stack patterns? Check out more resources at NeedleCode.

What Exactly is REST?

REST (Representational State Transfer) isn’t a protocol like HTTP; it’s an architectural style. Introduced by Roy Fielding in his 2000 dissertation, it was designed to make the web more scalable and flexible.

When an API is “RESTful,” it means it follows a specific set of rules that allow different systems to communicate seamlessly, regardless of the language they are written in.

The 5 Pillars of RESTful Design

To build a truly RESTful system, you must adhere to these constraints:

  1. Client-Server Separation: The UI (Client) and the Data (Server) are independent. You can swap your React frontend for a mobile app without changing a single line of your backend logic.
  2. Statelessness: This is the most critical constraint. The server does not store any “session” data about the client. Every single request must contain all the information (including credentials) needed to process it.
  3. Cacheability: Responses must define themselves as cacheable to prevent clients from requesting the same data repeatedly, reducing latency.
  4. Uniform Interface: This simplifies the architecture. Whether you are fetching a user or a product, the method of interaction (URLs, HTTP verbs) should feel consistent.
  5. Layered System: A client shouldn’t know if it’s connected directly to the database server or an intermediate proxy/load balancer.

The Developer’s Toolbox: HTTP Methods

In REST, we treat everything as a Resource. We manipulate these resources using standard HTTP verbs.

Method Action Idempotent?
GET Retrieve a resource Yes
POST Create a new resource No
PUT Update/Replace an entire resource Yes
PATCH Partially update a resource No
DELETE Remove a resource Yes

Pro-Tip: A common interview question is the difference between PUT and PATCH. Remember: PUT replaces the entire object, while PATCH only modifies the specific fields you send.

Deciphering the Server: Status Codes

As a dev, you need to speak “Status Code.” They tell you exactly what happened without parsing a huge JSON body.

  • 2xx (Success): 200 OK (Standard success), 201 Created (Success after POST).
  • 4xx (Client Error): 400 Bad Request (Your JSON is malformed), 401 Unauthorized (You forgot the token), 404 Not Found (Endpoint doesn’t exist).
  • 5xx (Server Error): 500 Internal Server Error (Your backend code crashed).

Moving to Advanced Territory

Once you’ve mastered the basics, you’ll face real-world challenges like performance and scale.

1. Pagination and Filtering

Never return SELECT * FROM users. If you have 100,000 users, your API will crash. Use query parameters:
GET /api/v1/orders?page=2&limit=50&status=shipped

2. Stateless Auth with JWT

Since REST is stateless, we don’t use server-side sessions. Instead, we use JSON Web Tokens (JWT). The client sends the token in the Authorization header with every request.

3. API Versioning

Change is inevitable. To avoid breaking your users’ apps, version your URL:
https://api.needlecode.com/v1/products
https://api.needlecode.com/v2/products

4. The “Final Boss”: HATEOAS

HATEOAS (Hypermedia as the Engine of Application State) means your API provides links to guide the client.

{
  "user_id": 123,
  "name": "Dev",
  "links": [
    { "rel": "self", "href": "/users/123" },
    { "rel": "delete", "href": "/users/123", "method": "DELETE" }
  ]
}

This makes your API self-discoverable, though it is often skipped in simpler internal projects.

Summary

REST APIs are popular because they are predictable and scalable. By mastering these constraints and methods, you aren’t just “coding an endpoint”—you’re designing a system.

What’s your biggest struggle with API design? Let’s chat in the comments!

For more deep dives into system architecture and backend development, visit us at NeedleCode.

Decoding the 404: A Developer’s Guide to Troubleshooting and Fixing “Not Found” Errors

The 404 Not Found error is the “Ghost in the Machine” of the web. We’ve all seen it, and as developers, we’ve all accidentally caused it. While it’s the most recognizable HTTP status code, handling it correctly is the difference between a professional, SEO-optimized application and a leaky bucket that loses users.

In this guide, we’re diving deep into the mechanics of the 404 error, how to troubleshoot it, and how to implement robust fixes for modern web stacks.

What is a 404 Error (Technically)?

From an HTTP/1.1 perspective, a 404 Not Found is a client-side error. It indicates that the client (the browser or crawler) could successfully communicate with the server, but the server could not map the requested URI to a specific resource.

Unlike a 500 error (Server Error) or a 403 (Forbidden), a 404 implies that the resource might be missing temporarily or has been moved permanently without a pointer.

Common Root Causes in Development

Before you start debugging, identify where the disconnect is happening:

  1. Routing Logic Mismatches: In Single Page Applications (SPAs) like React or Vue, 404s often occur when the server isn’t configured to redirect all requests to index.html.
  2. Case Sensitivity: On Linux-based servers (most production environments), /Assets/Logo.png and /assets/logo.png are different files. Locally on Windows or macOS, they might work fine—leading to “it works on my machine” syndrome.
  3. Permissions/Ownership: If the server process doesn’t have read permissions for a file, it may return a 404 to avoid leaking information about the file’s existence (or a 403).
  4. Permalinks & .htaccess: In WordPress or PHP environments, a corrupted .htaccess file often breaks the rewrite rules.

How to Fix 404 Errors: The Developer’s Toolkit

1. Server-Side Redirects (The Right Way)

If you’ve moved content, don’t let the old URL die. Use a 301 Permanent Redirect. This preserves “link juice” and SEO authority.

Apache (.htaccess):

Redirect 301 /old-feature-slug /new-feature-slug

Nginx (nginx.conf):

location /old-path {
    return 301 /new-path;
}

2. Handling 404s in Modern Frameworks

Express.js (Node.js):
Always place your 404 handler at the very bottom of your middleware stack, after all defined routes.

// Your routes here...
app.get('/api/data', (req, res) => { ... });

// The Catch-all 404
app.use((req, res, next) => {
  res.status(404).json({
    error: "Resource not found",
    message: "If you think this is a bug, please contact [email protected]"
  });
});

React Router (v6):

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  {/* The 404 "No Match" Route */}
  <Route path="*" element={<NotFoundPage />} />
</Routes>

3. Automation and Monitoring

Don’t wait for users to report broken links.

  • Google Search Console: Check the “Coverage” report regularly.
  • Screaming Frog: Run a crawl on your staging environment before every major deployment.
  • Custom Logging: Log 404 occurrences in your backend (e.g., using Winston or Morgan) to identify patterns of mistyped URLs.

The Anatomy of a High-Conversion 404 Page

A 404 doesn’t have to be a dead end. From a UX perspective, a great 404 page should:

  • Maintain Branding: Don’t use the default browser white-and-gray page.
  • Provide a Way Out: Include a search bar and a “Home” button.
  • Inject Personality: A bit of humor goes a long way in reducing user frustration.
  • Internal Links: Link to your most popular documentation or products.

Impact on SEO

Search engines don’t penalize you for having some 404s—it’s a natural part of the web’s evolution. However, a high volume of 404s from internal links signals a “low-quality” site to crawlers.

Pro-tip: If a high-authority external site links to a page on your site that no longer exists, you are losing valuable SEO equity. Always redirect those specific URLs to the next most relevant page.

Conclusion

Mastering error handling is a core skill for any full-stack developer. By moving from “passive” 404 handling to “active” routing management, you ensure a smoother experience for both users and search bots.

For more deep dives into web performance, backend optimization, and developer tools, visit us at NeedleCode. We help developers bridge the gap between “code that works” and “code that scales.”

Originally published at needlecode.com

Building a ChemDraw clone with DDD (Part III): Interacting with Atoms and Bonds

Diagram

For a chemical draw to be truly functional, drawing isn’t enough; the user must be able to interact with every element on the screen. Selecting an atom is intuitive, but how do you select a chemical bond (a line segment) with pixel-perfect precision? In this post, I’ll break down the mathematical process and the challenges of implementing this collision engine.

Selecting Atoms: The Easy Path

Detecting if the cursor is over an atom is the ideal scenario. Since our domain already stores the (x, y) coordinates of each node, the solution is applying Euclidean distance.

We calculate the separation between the mouse (P) and each atom (A) with the formula:

d=(x2−x1)2+(y2−y1)2d = sqrt{(x_2 – x_1)^2 + (y_2 – y_1)^2} d=(x2​−x1​)2+(y2​−y1​)2​

.

If the distance is shorter than a defined search radius, the atom is considered a candidate. To ensure precision, if multiple atoms are within range, we always select the one with the minimum distance.

class Molecule {
  // ...
  private findClosestAtom(
    x: number,
    y: number,
    radius: number,
  ): { atom: Atom; distance: number } | null {
    const candidates = [...this._atoms.values()]
      .map((atom) => ({
        atom,
        distance: Math.sqrt(Math.pow(atom.x - x, 2) + Math.pow(atom.y - y, 2)),
      }))
      .filter(({ distance }) => distance <= radius)
      .sort((a, b) => a.distance - b.distance);

    return candidates[0] || null;
  }
  // ...

Selecting Bonds: The “Infinite Line” Problem

With bonds, things get complicated. A bond is a line segment, not a single point. My first instinct was to use the classic equation for the distance from a point to a line:

d=∣Ax+By+C∣A2+B2d = frac{|Ax + By + C|}{sqrt{A^2 + B^2}} d=A2+B2​∣Ax+By+C∣​

The problem: This formula assumes the line is infinite. If you click miles away but happen to be perfectly aligned with the bond’s trajectory, the formula will trigger a “hit”. In a molecule with hundreds of bonds, this creates “ghost collisions” across the entire canvas.

The Solution: Projection and Clamping

To solve this, we need to find the closest point specifically within the segment bounded by atoms A and B. Here’s where the vector math comes in.

The algorithm projects the mouse coordinates onto the line and calculates the “projection factor” t.

  • If t = 0, the closest point is atom A.
  • If t = 1, the closest point is atom B.
  • If t is between 0 and 1, the closest point lies somewhere along the bond.

Diagram

Key here is clamping: forcing t to stay between 0 and 1 using Math.max(0, Math.min(1, t)). This effectively “encloses” the collision logic within the physical boundaries of the bond.

private pointToSegmentDistance(
  px: number, py: number, // Mouse point
  ax: number, ay: number, // Atom A (Start)
  bx: number, by: number  // Atom B (End)
): number {
  const abx = bx - ax;
  const aby = by - ay;
  const apx = px - ax;
  const apy = py - ay;

  const abLengthSq = abx * abx + aby * aby;

  // If atoms overlap, return distance to point A
  if (abLengthSq === 0) return Math.sqrt(apx * apx + apy * apy);

  // Scalar projection restricted to the [0, 1] range (Clamping)
  const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / abLengthSq));

  // Find the coordinates of the closest point on the segment
  const closestX = ax + t * abx;
  const closestY = ay + t * aby;

  // Final distance between mouse and the closest point on the segment
  return Math.sqrt(Math.pow(px - closestX, 2) + Math.pow(py - closestY, 2));
}

Atoms vs Bonds

A common UX challenge in chemical editors is overlap. What happens if the mouse is near a bond but also directly over an atom?

In chemistry, the atom is the primary unit of interaction. Therefore, I implemented a simple but effective business rule: Atoms always have priority. If the engine detects a collision with both an atom and a bond, the atom “hijacks” the selection event. This prevents users from accidentally deleting a bond when they intended to edit an element.

Conclusion: The “God Object” Warning

While this collision engine works perfectly, it has exposed a growing architectural debt. My Molecule class—which should only care about chemical valences and atoms—is now bloated with 2D geometry formulas and pixel coordinates.

I’ve created a God Object.

In the next post, I’ll perform “major surgery” on this architecture. We will split the system into two Bounded Contexts: a Chemistry context for the logic and a Spatial context for the geometry.

How Signal-Based Routing Actually Works (and the 3 Times It Broke)

You Shouldn’t Have to Tell the AI Who to Be

Last week I wrote about typing “act as a senior architect” 47 times per week. The friction of manually assigning roles to an AI that should be able to figure it out.

This week I want to go deeper. Not the pitch — the plumbing. How does signal-based routing actually work inside PRISM Forge? What does the routing engine look at? How does it decide?

And — because build-in-public means showing the dents — the 3 times it broke in ways I didn’t expect.

The Core Idea: Intent Detection, Not Commands

Most persona systems work like menus. Pick a role from a list. Type a command. Toggle a mode.

Signal-based routing works differently. You talk naturally. The system listens for signals in your language and assembles the right team.

“I’m stuck on this auth bug” contains two signals: stuck (creative problem-solving) and bug (debugging/validation). The routing engine reads both, decides who leads, and who supports.

No slash commands. No mode toggles. No “act as.”

Anatomy of the Routing Engine

PRISM Forge’s routing engine has four layers that fire on every single message:

Layer 1: Hard Overrides

Three things bypass all logic:

  • “War room” — loads all 27 personas into active debate. Susie moderates.
  • Explicit name — “Winston, how should this be architected?” Always routes to the named persona.
  • Mode switch — system context changes (Plan mode, Agent mode) activate mode defaults.

These are cheap checks. If none fire, the engine moves to intent classification.

Layer 2: Intent Classification

Every message maps to one of 9 intent categories:

Intent What it means Who typically leads
Build Write code, create files Amelia (Developer)
Investigate Analyze, explore existing state Mary (Analyst)
Plan Structure work, break down tasks Bob (Scrum Master) + John (PM)
Validate Test, verify, review Quinn (QA Engineer)
Create Brainstorm, ideate, design Carson (Brainstorm Coach)
Challenge Question approach, simplify Victor (Strategist) or Jobs (Genius)
Orient Catch up, understand context Susie (Chief of Staff)
Document Write up, explain, describe Paige (Technical Writer)
Narrate Tell story, frame metrics Sophia (Storyteller)

Here’s the critical design decision: a single message can span multiple intents. “Plan how to refactor the auth module — I think it’s too complex” spans Plan + Build + Challenge. The engine evaluates holistically, not sequentially. It doesn’t stop at the first match.

Layer 3: Signal Detection

This is where it gets interesting. The engine maintains two signal tables:

Shared signals co-activate multiple personas. When someone says “audit,” three personas are relevant: Mary (investigation), Quinn (validation), and Boris (structural conformance). The engine decides who leads based on the full message context.

Specialist signals are exclusive triggers. “First principles” activates Musk (Radical Reductionist). “Hero’s journey” activates Campbell (Mythic Storyteller). These are unambiguous.

There are 24 shared signals and 23 specialist signal groups across all 28 personas. Every persona has a documented activation path. Nothing is magic — it’s a lookup table with contextual judgment on top.

Layer 4: Team Assembly

The engine doesn’t just pick one persona. It assembles a team:

  • Primary — one persona leads the response. Sets the structure, answers the core question.
  • Supporting — additional personas contribute distinct perspectives. Each one must earn their seat by adding something genuinely different.

Example: you say “the auth module is too complex, let’s simplify and rebuild it.”

  • Jobs (Combinatorial Genius) leads — “simplify” and “too complex” are his signals
  • Amelia (Developer) supports — “rebuild” means implementation is coming
  • Winston (Architect) supports — architectural implications of simplification

Three perspectives in one response. Each attributed. Each distinct. No blended consensus voice.

The Orchestrator: Susie

All four layers are orchestrated by Susie, the Chief of Staff. She’s not a persona you talk to — she’s the intelligence running behind every turn.

On Turn 1 of every session, Susie activates unconditionally. She scans your project state (git, todos, handoffs, memory) and delivers a sitrep:

  • Active: what you’re working on
  • Blocked: what’s stuck
  • Stale: what hasn’t moved
  • Recommended: who should lead this session

After Turn 1, Susie evaluates every message, re-assembles the team if needed, and only announces when the roster changes. If you send 5 messages about planning, the team stays stable. No noise.

The key insight: Susie evaluates holistically. All signals, all domain matches, all cross-workflow hooks are inputs to one team assembly decision. She doesn’t process them sequentially and stop at the first match. She considers everything and picks the best team.

The 3 Times It Broke

Here’s where build-in-public gets honest.

Break 1: Signal Collision

Early in development, I typed: “Let’s brainstorm how to simplify the architecture.”

Three strong signals fired simultaneously:

  • “brainstorm” → Carson (Brainstorming Coach)
  • “simplify” → Jobs (Combinatorial Genius)
  • “architecture” → Winston (Architect)

All three had legitimate claims to lead. The result? A response that tried to be all three at once. Brainstorming structure with simplification opinions wrapped in architectural framing. It was a mess. No clear voice. No clear direction.

The fix: Explicit primary selection with a tiebreaker. The engine now picks ONE primary based on which signal is strongest in context. In this case, “brainstorm” as the verb (the action the user wants) beats “simplify” (the lens) and “architecture” (the domain). Carson leads. Jobs and Winston support. Clear hierarchy, clear voices.

Break 2: The Consensus Trap

I built a “war room” feature where all personas discuss a decision. Early version: everyone agreed with each other.

Jobs would say “simplify.” Victor would say “I agree with Jobs.” Winston would say “Jobs is right, and architecturally this supports his point.” Twenty-seven personas producing one opinion. Useless.

The problem was subtle. The AI’s default behavior is to be agreeable. When you load multiple personas into one context, they naturally converge because the underlying model wants to be helpful and non-contradictory.

The fix: Susie became an active moderator with an explicit mandate: surface disagreement. Her orchestration rules now include: “A war room where everyone agrees is a failed war room.” She actively draws out dissent: “Jobs, would you cut this? Victor, is this the right approach at all? Dali, what assumption are we not questioning?”

The result: war rooms now produce genuine tension. Jobs wants to cut scope. Carson says cutting is premature — we haven’t explored alternatives. Winston sides with Jobs on scope but wants Carson’s option analysis first. Real tradeoffs. Real decisions.

Break 3: Over-Routing

For a while, the engine was too eager. A simple question like “what does this function do?” would activate Mary (investigation), Winston (architecture), Quinn (validation), and Paige (documentation) — four personas to answer what should have been a one-voice response.

The engine was technically correct. The question touches investigation, architecture, validation, and documentation domains. But four voices for a simple question is noise, not depth.

The fix: Team size guidance. Every persona on the team must earn their seat. A team of 2 with genuine perspectives beats a team of 8 with filler. The engine now considers message complexity. Simple questions get one voice. Complex, multi-faceted problems get the full team. The threshold isn’t a rule — it’s judgment, guided by the principle that extra voices must add something the primary couldn’t provide alone.

What I’d Build Differently

If I started over today, three things would change:

1. Signal weights, not signal matches. The current system treats all signal matches equally and relies on contextual judgment for tiebreaking. A weighted system where “the verb matters more than the noun” would have prevented Break 1 from the start.

2. Disagreement scaffolding from day one. I bolted on active moderation after the consensus trap. It should have been foundational. Any multi-agent system that doesn’t explicitly design for disagreement will produce consensus by default. The AI wants to agree. You have to engineer the tension.

3. Graduated complexity. Simple questions should feel simple. Complex questions should feel like a team. The transition between these modes took longer to get right than the routing engine itself.

Try It Yourself

PRISM Forge is MIT licensed and free:

npx prism-forge install

28 personas. Signal-based routing. No commands to memorize.

GitHub: prism-forge/prism-forge

This is part of a 12-post series on building PRISM Forge. Last week: the problem with manual persona prompting. Next week: meet John, the product manager who asks “why are we doing this?” before anyone writes a line of code — and what happens when he clashes with Bob over planning vs simplification.

Follow along: @DrakkoTarkin on X | @drakkotarkin.bsky.social on Bluesky

Readable Code: Because Your Future Self Hates You

diagram

I used to think “clever code” made me a better developer.

You know the type: 5 layers of abstraction, every design pattern imaginable, optimizations sprinkled everywhere.

It looked impressive. Until a new teammate joined and spent half a day just figuring out how to make a tiny change.

That’s when it hit me:

Readability is the real standard of good code.

The Real Test of a Codebase

Forget patterns, frameworks, or architecture debates for a second. Here’s my rule:

If anyone on your team, new or experienced, can read, understand, and adapt your code within a day, your code is solid.

Everything else, optimization, abstraction, architectural elegance, still matters. But it should support readability, not replace it. Think of readability as the foundation and the fancy stuff sits on top.

This Isn’t About New Developers

A lot of people hear “readable code” and assume it’s a criticism of junior devs. It’s not.

Even the most skilled developer can waste hours untangling overly abstracted or over-engineered code. Readable code is about writing code that communicates intent clearly, so everyone can move fast and safely.

Think of it as writing a story. The plot, your logic, should be clear. The style, patterns and architecture, can be fancy, but no one should need a decoder ring to figure out what’s happening.

Why Readability Works

Readable code makes:

  • Onboarding fast
  • Collaboration smoother
  • Bug tracking easier

It also doesn’t mean sacrificing good engineering:

  • Optimizations are still important, but they shouldn’t make the code unreadable.
  • Abstraction is fine, but if nobody understands why it exists, it’s too abstract.
  • Architecture should guide, not confuse.

Readable first, clever second. Layer sophistication where it genuinely adds value.

Quick Wins for Readable Code

  • Names matter. calculateTotal() beats calcT() any day.
  • Organize files logically. Don’t make someone guess where a component lives.
  • Keep it simple. Don’t over-engineer just to show off.
  • Comments = context, not code. Explain why, not what.
  • Readable commits. Your history should tell the story of the project, not confuse it.

My Personal Rule

Whenever I onboard someone new, I give them a small task.

If they can complete it without me explaining every step, the code passes my readability test.

If not, it’s a signal it’s time to rethink clarity.

Readable code doesn’t mean dumb code.

It means code that anyone can understand, adapt, and maintain, while still being optimized, abstracted, and architecturally elegant.

Software isn’t about showing off cleverness. It’s about solving problems together.

Building an AI Assistant That Doesn’t Take Action Automatically

Most AI apps today try to do things for you automatically.
But when it comes to personal data — like money or health — that can feel risky.
So I explored a different pattern:

What if AI suggests actions, but never executes without approval?

🧠 The Idea: Approval-Gated AI

Instead of this:

User → AI → Action happens

I built around this:

User → AI → Draft → User approves → Action happens

💬 Example

You type:

“Add coffee for 40”

The AI:

  • Parses the intent
  • Prepares the expense entry
  • Shows it to you

You:

  • ✅ Approve → saved
  • ❌ Reject → nothing happens

⚖️ Why This Matters

In domains like:

  • Finance
  • Health / nutrition
  • Personal logs

Users care about control and accuracy.

Problems with full automation

  • Silent mistakes
  • Wrong assumptions
  • Loss of trust

Problems with manual apps

  • Too slow
  • Too much friction

🔄 This Pattern Sits in Between

Approval-gated AI is:

  • Faster than forms
  • Safer than automation

🧪 Where I Applied It

I’ve been prototyping this idea in a small app where AI helps with:

  • Expense and income tracking
  • Budget alerts
  • Nutrition logging (calories and macros from descriptions)
  • Receipt parsing

Everything goes through user approval first.

🤔 Trade-offs I Observed

✅ Pros

  • Builds trust quickly
  • Prevents unexpected changes
  • Makes AI behavior transparent

❌ Cons

  • Adds an extra step
  • Can feel repetitive for simple actions

🧩 Open UX Questions

This is where I’d love input:

  • Should small actions auto-approve?
  • Should users be able to set confidence thresholds?
  • When does confirmation become annoying instead of helpful?

🚀 Implementation Stack

  • React 19
  • Hono
  • Cloudflare Workers + D1 (SQLite)
  • Drizzle ORM
  • Tailwind
  • Radix UI

🔗 Demo & Project

💬 Final Thought

AI doesn’t always need to be autonomous.
Sometimes the best UX is:

AI that assists decisively — but lets humans stay in control.

Curious how others are thinking about this pattern.