Why I Teach Python With a City Simulator (And Why You Should Build One Too)

There’s a wall that nobody warns you about.

You finish a Python basics course. You understand variables, loops, functions, maybe classes. You open a blank file, ready to build something real.

And nothing happens.

Not because you’re bad at this. Because the jump from “I understand the concepts” to “I can build something” is itself a skill — and it’s one that almost no tutorial teaches.

Tutorials teach decomposition: here is a loop, here is a function, here is a class. The implicit promise is that once you’ve collected enough topics, you’ll be able to assemble them into something real. But assembly is a skill. And the only way to learn it is to build something that grows over a long arc, accumulates history, and fights back.

The question is: what do you build?

Not a todo list. Not a temperature converter. Something with stakes. Something where the decision you make in week two still matters in week six.

My answer: a simulation.

What Makes a Simulation Different

Most beginner projects have a simple shape: input goes in, output comes out, the program stops. A simulation has a completely different shape: it has state that persists, rules that transform that state over time, and events that keep it from being predictable.

Those three ingredients turn out to be exactly what you need to learn the things tutorials can’t teach.

State is the set of values that describe your system at a given moment. A treasury balance. The condition of each building. The morale of each citizen. The day counter. Everything the simulation needs to know to continue from where it is.

Rules are the logic that transforms state over time. Each day, building condition decreases slightly. Each season, happiness shifts. Each tick, wages are paid before taxes are collected. Rules are deterministic — given the same state, they produce the same result. But they’re also composable: the interaction between three simple rules can produce behaviour that wasn’t written anywhere explicitly.

Events are the stochastic element that makes every run different. A fire breaks out. An investor arrives. A citizen gets promoted unexpectedly. Events prevent the simulation from settling into a predictable cycle.

Here’s what that looks like in the simplest possible Python:

# State
city = {
    "treasury": 50000,
    "building_condition": 100,
    "day": 0
}

# Rules
def tick_day(city):
    city["day"] += 1
    # decay rate accelerates as condition falls — a neglected building deteriorates faster
    decay = 0.5 + (100 - city["building_condition"]) * 0.05
    city["building_condition"] -= decay
    if city["building_condition"] < 0:
        city["building_condition"] = 0
    # maintenance cost also rises as condition falls
    maintenance = (100 - city["building_condition"]) * 10
    city["treasury"] -= maintenance
    return city, maintenance, decay

# Run it — print every 5 days
print(f"{'Day':>4} | {'Condition':>9} | {'Decay':>6} | {'Daily Cost':>10} | {'Treasury':>12}")
print("-" * 58)
for i in range(60):
    city, cost, decay = tick_day(city)
    if (i + 1) % 5 == 0:
        print(f"{city['day']:>4} | {city['building_condition']:>9.1f} | {decay:>6.2f} | ${cost:>9.0f} | ${city['treasury']:>11,.0f}")

Run that and watch what happens across 60 days. On day 5 the daily maintenance cost is $28. By day 30 it’s $332. By day 50 the building hits zero condition and the city is losing $1,000 every single day — bleeding $5,000 from the treasury every five days with no end in sight.

You wrote two rules. Decay accelerates as condition falls. Cost rises as condition falls. Each rule makes the other worse. That compounding interaction — not written anywhere explicitly — is the feedback loop. The treasury isn’t just draining. It’s accelerating toward collapse. And the city hasn’t even had a bad event yet.

That emergence is the point.

The Bugs Are Different

Here’s what nobody tells you about simulation bugs: they don’t crash.

A bug in a normal script announces itself. The program stops, Python prints a traceback, you fix it. A simulation bug produces the wrong numbers quietly. The city runs fine. Citizens appear to be doing things. But morale is trending wrong, or the treasury is draining faster than it should, or events aren’t firing at the right frequency.

To find those bugs you can’t just read a traceback. You have to reason about state. What values did this function receive? What should it have returned? What rule is interacting with what other rule to produce this drift?

That reasoning — sitting with a running system and building a mental map of its behaviour — is exactly the skill that professional debugging requires. Real codebases don’t crash cleanly either. They produce wrong outputs, subtle regressions, behaviour that only appears under specific conditions. The muscle you build debugging a simulation is directly transferable.

Why a City Specifically

The inspiration for the book I eventually wrote was Championship Manager — a 1990s football management simulation. You don’t play a character directly. You sit one level above the action, make decisions, and watch a simulation engine translate those decisions into results over time.

Those games weren’t famous for their graphics. They were famous for making you care.

You approved a new residential block in year two because the treasury looked healthy. By year four the roads serving it are at 30% condition, commute times have doubled, and three citizens are considering leaving. You don’t blame bad luck. You trace the decision back.

A city has everything a good teaching simulation needs:

  • Real state: population, infrastructure condition, economic health, political mood
  • Real rules: maintenance costs, population growth patterns, economic feedback loops
  • Real events: disasters, windfalls, elections, crises
    Nothing is metaphorical. It’s a simplified model of actual dynamics. And every concept you encounter — object-oriented design, data structures, algorithms, testing — has an obvious real-world analogy in the city.

When you discover that a building is not just a name but a collection of named facts — condition, age, type, maintenance cost — dictionaries become natural, not abstract. When you need to track a citizen across many days, classes stop being a syntax exercise and become the obvious tool.

What Accumulation Teaches

The deepest thing a long-arc project teaches is something you can’t get from isolated exercises: how systems age.

Code you write in chapter 4 is still running in chapter 14. Decisions you made early create constraints later. A function that seemed fine when it did one thing starts to resist change when you need it to do three things. You feel why small, focused functions matter — not because someone told you, but because yours got unwieldy and you had to fix it under pressure.

Consider this: in the book I wrote, there’s a citizen named Tom. He’s 63 when you first create him in chapter 4. By chapter 10 he retires. By chapter 12 his retirement is recorded in the City Chronicle — an append-only log of significant events. By chapter 14 a sparkline shows his morale trend across the years he worked.

Tom is not a real person. He’s a named variable with richer state than most named variables. But when the simulation surfaces that another citizen, Elena, couldn’t cover her rent last Tuesday, you notice in a way you wouldn’t if it said “Citizen 4: rent shortfall.” Names create the minimum viable fiction required for a simulation to generate the feeling of consequence.

And consequence is what makes the learning stick.

The Terminal as Canvas

One deliberate choice in the book: everything runs in the terminal. No web framework, no GUI library, no browser. Just Python and a terminal.

The terminal is universal — it runs identically on macOS, Linux, and Windows without installation beyond Python itself. But with the Rich library, it becomes genuinely expressive: colour, tables, panels, progress bars, live-updating layouts.

By chapter 8 of the book, the simulation runs inside a five-region live dashboard that updates in real time:

┌─────────────────────────────────────────────────────────────┐
│  NEW PYTHONVILLE   Day 847  |  Spring  Year 3               │
├──────────────┬──────────────┬──────────────┬────────────────┤
│  TREASURY    │  CITIZENS    │  BUILDINGS   │  EVENTS        │
│  $47,230     │  6 residents │  8 buildings │  Quiet day     │
│  ▼ trending  │  morale: 72  │  avg: 84%    │                │
├──────────────┴──────────────┴──────────────┴────────────────┤
│  CHRONICLE                                                   │
│  > Tom Wick retired after 847 days of service               │
│  > Elena Kosta promoted to Senior Engineer                  │
└─────────────────────────────────────────────────────────────┘

The terminal is also honest. There’s nowhere to hide. If your dashboard renders wrong, you see it immediately. That short feedback loop between code and result is one of the most valuable things a learning environment can have.

How to Start Your Own

You don’t need the book to start a simulation project. You need three things:

1. Pick a system you actually find interesting.
Cities work well. So do football leagues, ecosystems, stock markets, hospital wards, ant colonies. The subject doesn’t matter. What matters is that it has things that change over time and decisions that have consequences.

2. Start with the absolute minimum viable state.
Don’t design the whole thing upfront. Start with one entity, one rule, one event. Get it running. Then add one more thing. Let the architecture emerge from what the simulation needs rather than what you planned.

3. Name things.
Not entity_1 and entity_2. Names. The minimum viable fiction that makes you care about what happens. You’ll debug harder, think more carefully, and learn more from a system whose inhabitants feel real.

The rest — data structures, algorithms, testing, optimisation — will arrive naturally when the simulation demands them. That’s the whole point.

The Book

I spent two months building the structured version of this idea: Masters of the Metropolis, a simulation-first introduction to Python across 18 chapters. You build a city simulator from scratch — one layer at a time — until you have a live terminal dashboard, a seasonal economy, citizens with life arcs, save and load, elections, testing infrastructure, and a City Chronicle that makes every run unique.

If you’ve finished a Python basics course and don’t know what to build next, this is that next step.

leanpub.com/metropolis — free sample available, no account needed.

What would you build a simulation of? Drop it in the comments — I’m genuinely curious what systems people find interesting.

LeetCode Solution: 5. Longest Palindromic Substring

Longest Palindromic Substring: A Core String Algorithm Explained

Welcome to another deep dive into a classic LeetCode problem! Today, we’re tackling Problem 5: Longest Palindromic Substring. This problem is a fantastic way to sharpen your string manipulation and algorithmic thinking skills.

Problem Explanation

Given a string s, our goal is to find and return the longest substring within s that is a palindrome.

What’s a palindrome? It’s a sequence of characters that reads the same forwards and backward (e.g., “madam”, “racecar”, “level”).
What’s a substring? It’s a contiguous sequence of characters within a string (e.g., for “apple”, “app” is a substring, but “ale” is not).

Let’s look at the examples:

  • Example 1:

    • Input: s = "babad"
    • Output: "bab"
    • Explanation: "aba" is also a valid answer of the same length.
  • Example 2:

    • Input: s = "cbbd"
    • Output: "bb"

The string s will contain only digits and English letters, and its length will be between 1 and 1000 characters.

Intuition

The core idea behind finding palindromes is that they expand symmetrically from a center point. Think about “racecar” – the ‘e’ is the center, and ‘c’ expands to ‘r’. Or “abba” – the center is between the two ‘b’s, and ‘a’ expands to ‘a’.

This insight simplifies our search: instead of checking every single substring for palindromic properties (which is O(N^3) or O(N^2) with an O(N) check), we can iterate through every possible “center” and try to expand outwards.

There are two types of centers we need to consider:

  1. Single character center: For palindromes with an odd length (e.g., “b*ab”, “ra*cecar”). Here, s[i] itself is the center.
  2. Two character center: For palindromes with an even length (e.g., “bb”, “a*bb*a”). Here, s[i] and s[i+1] form the center.

Approach

We’ll use an “Expand Around Center” approach:

  1. Initialize: Keep track of the start and end indices of the longest palindrome found so far. Initialize them to 0, 0 (assuming a single character is the shortest possible palindrome).
  2. Iterate Through Centers: Loop through each character in the string using an index i. Each i will serve as a potential center.
  3. Expand for Odd Length Palindromes: Call a helper function expand_around_center(s, i, i). This tries to expand a palindrome assuming s[i] is its sole center.
  4. Expand for Even Length Palindromes: Call the helper function expand_around_center(s, i, i + 1). This tries to expand a palindrome assuming s[i] and s[i+1] are its two central characters.
  5. Helper Function expand_around_center(s, left, right):
    • This function takes the string s and two indices, left and right.
    • It expands outwards: left moves to the left (left-1) and right moves to the right (right+1).
    • This expansion continues as long as left is within bounds (>= 0), right is within bounds (< len(s)), and the characters at s[left] and s[right] are equal.
    • Once the loop terminates (either due to bounds or mismatch), the length of the palindrome found is right - left - 1.
  6. Update Longest Palindrome: After getting len1 (odd center) and len2 (even center), determine max_len = max(len1, len2). If max_len is greater than the length of our current longest palindrome (end - start + 1), then update start and end.
    • To calculate the new start and end from i and max_len:
      • start = i - (max_len - 1) // 2
      • end = i + max_len // 2
  7. Return Result: After iterating through all possible centers, return the substring s[start : end + 1].

Code

class Solution:
    def longestPalindrome(self, s: str) -> str:
        if not s:
            return ""

        start = 0
        end = 0

        # Helper function to expand around a given center
        def expand_around_center(s: str, left: int, right: int) -> int:
            while left >= 0 and right < len(s) and s[left] == s[right]:
                left -= 1
                right += 1
            # When the loop finishes, left and right are one step beyond the palindrome boundaries.
            # So, the length is (right - 1) - (left + 1) + 1 = right - left - 1
            return right - left - 1

        # Iterate through each character, treating it as a potential center
        for i in range(len(s)):
            # Case 1: Odd length palindrome (center is s[i])
            len1 = expand_around_center(s, i, i)

            # Case 2: Even length palindrome (center is s[i] and s[i+1])
            len2 = expand_around_center(s, i, i + 1)

            # Get the maximum length found from these two expansions
            max_len = max(len1, len2)

            # If this palindrome is longer than our current longest
            if max_len > end - start + 1:
                # Calculate the new start and end indices
                # For an odd length palindrome centered at i: start = i - (length-1)/2, end = i + (length-1)/2
                # For an even length palindrome centered between i and i+1: start = i - (length/2 - 1), end = i + length/2
                # The combined formula works for both:
                start = i - (max_len - 1) // 2
                end = i + max_len // 2

        # Return the longest palindromic substring
        return s[start : end + 1]

Time & Space Complexity Analysis

  • Time Complexity: O(N^2)

    • The main loop iterates N times, where N is the length of the string s.
    • Inside the loop, expand_around_center is called twice. In the worst case, expand_around_center might traverse the entire string (e.g., for “aaaa…a”).
    • Thus, each call to expand_around_center takes O(N) time.
    • Overall complexity: N * O(N) = O(N^2).
  • Space Complexity: O(1)

    • We are only using a few variables (start, end, i, len1, len2, max_len, left, right) to store indices and lengths.
    • The space used does not grow with the input string size.

Key Takeaways

  • Palindromes and Symmetry: The inherent symmetry of palindromes is key to efficient algorithms.
  • Expand Around Center: This is a powerful technique for palindrome problems, allowing us to find palindromes by growing them from every possible middle point.
  • Handling Odd and Even Lengths: Remember to consider both types of palindromes (odd-length with a single character center, and even-length with a two-character center).

This “Expand Around Center” approach is generally simpler to implement than dynamic programming solutions for this problem, while achieving the same O(N^2) time complexity.

Author Account: p1Hzd8mRM8
Time Published: 2026-05-17 00:13:54

I told my AI to build a feature. Did it? I had no idea.

Screenshot of docs/dev folder with 56 markdown files using proposal-, review-, and handoff- naming patterns

TL;DR — I tried to “manage” AI by having it write decisions, todos, and constraints into markdown docs. After 56 files, I realized AI doesn’t maintain document state. So I built aming-claw — a backlog database AI can actually read and write through MCP.

A bug I kept running into

I thought I was doing AI collaboration the right way.

Screenshot of docs/dev folder with 56 markdown files using proposal-, review-, and handoff- naming patterns

This is the docs/dev/ folder of my aming-claw project — 56 markdown files, all produced through AI collaboration:

  • proposal-* — new feature specs
  • review-* — design review records
  • handoff-* — state passed between sessions
  • Plus plan-, optimization-, interface-, manual-fix-…

Every file dated. Two months in, over a thousand pages of markdown. I figured the next AI session would read these. I figured I’d be able to search them too.

But there’s one problem I can’t engineer my way out of:

AI doesn’t maintain document state.

  • proposal-graph-state-reconcile-and-chain-governance-modes.md — did this proposal ship? Which commit? Is it still valid?
  • handoff-2026-05-10-dashboard-semantic-hash-queue.md — did the next session actually pick up where this left off?
  • 18 proposals on file. Which are done, which got rejected, which are still alive? Grep through git log line by line?

I don’t manually maintain the docs, so the docs rot. AI doesn’t maintain them either — its context window only sees a tiny slice of the workspace. The other 56 files are invisible.

The more we talk, the more we write — and the further docs drift from code. Eventually you don’t trust the docs, and you don’t have time to read the code.

Why this happens

This isn’t AI being lazy. It’s a structural problem:

  1. Markdown is dead text. No state machine. “TODO” doesn’t become “DONE” on its own. “Decision: use Redis” doesn’t auto-expire when you flip back to in-memory three weeks later.
  2. AI context has a boundary. Each session sees ~200 lines of working code. Old docs never enter the window. Not in the window → can’t be maintained.
  3. No traceable link between docs and code. Which TODO maps to which function? Once it’s done, which commit landed it? Humans can’t remember. AI doesn’t look it up.

GitHub Issues, Notion, Linear — none of these help. AI can’t see them, so they don’t exist.

The core mismatch is this: humans want global state. AI sees only local present. Between them you need a living, traceable, AI-readable/writable state layer. Markdown isn’t that layer.

How aming-claw solves it

I gave aming-claw a dedicated backlog database — a peer-level system to the code graph and event ledger, with its own schema, state machine, and query interface. Not stored in markdown. Not buried in code comments. Not dependent on an external issue tracker.

Each backlog entry is a structured record (todo / decision / constraint) with status, priority, source session, and a code reference (function name or file path). AI reads and writes it through MCP.

The flow:

1. You speak → it goes to the database, not a dead doc

In chat:

“Add a retry-after to the rate limiter on UserService.login”

Or: “Decision — use Redis instead of in-memory for caching”

aming-claw’s MCP server intercepts those statements and writes directly into the backlog:

target:    UserService.login   # function or file path
type:      todo | decision | constraint
status:    proposed
priority:  P1
source:    session-id-xyz
timestamp: 2026-05-16T10:23:45Z

Markdown is dead text. The backlog database is live state — schema, indexed, state-machined, AI-accessible. That’s the difference.

2. Dashboard shows it instantly

Open the aming-claw dashboard — the left panel shows the new backlog entry. Click it — the right panel jumps to the function via the vscode:// protocol. Status chips are editable inline.

aming-claw dashboard backlog view showing multiple entries with priority, status, code references, and update timestamps

The backlog view — every entry has priority, status, code reference, and update timestamp. AI and you query the same source of truth.

3. State machine, automatic

proposed → in_progress → done(commit hash) → verified
  • in_progress — AI started working on it
  • done — commit landed, hash automatically bound
  • verified — you reviewed it

Every state change is appended to an event ledger: which day, which session proposed it, which commit implemented it, who verified it — all queryable, all replayable.

4. AI reads the backlog itself, next time

Days later, in chat:

“Did we ever fix that Codex plugin Windows install bug?”

AI queries the backlog through MCP and returns:

status:     FIXED, P0
commit:     0ad8c7e
fixed at:   2 days ago
file:       agent/plugin_installer.py (line 455)
change:     replaced regex pattern with callable replacement

No grepping git log. No asking a teammate. No “I think we did?”

aming-claw dashboard backlog view showing multiple entries with priority, status, code references, and update timestamps

The key thing to notice: AI didn’t “remember” this from conversation history. It queried the backlog database in real time through MCP. Even if this bug was raised three months ago, in a session that’s long gone — AI still gets the current status + full commit trace.

That’s the difference between dead markdown and a live state layer: the database is the memory, not the conversation.

This is just the start

Look back at the docs/dev/ screenshot — 56 markdown files, nobody knows which are alive.
Look at the dashboard screenshot — every backlog entry has status, commit, location.

The difference isn’t the tool. It’s whether information has state.

The backlog solves “did the AI build the feature I asked for?” — but AI collaboration has plenty of other holes I’m planning to fill in this series:

Pain Next article
AI edits one function, breaks 10 callers Code graph + impact analysis
AI modifies code it shouldn’t touch Governance hints
What did AI even change this week? Event ledger
Every session starts from zero Project memory layer

One article per pain point.

About aming-claw

  • GitHub: amingclawdev/aming-claw — open source
  • Next post: “AI breaks 10 callers when it edits one function” — coming this week
  • Hit me with issues if you’ve felt this pain

If “did the AI actually do that thing I asked?” sounds familiar, give the repo a star — it costs you nothing and tells me I’m not the only one.

This is part 1 of an “AI Collaboration Survival Guide” series — practical tools for the messy reality of building with AI agents.

Flutter Monetization Gets Messy Faster Than Most Developers Expect

Most Flutter apps start monetization with good intentions.

You add a banner ad.
Then a rewarded ad.
Then subscriptions.
Then premium suppression.
Then Remote Config.
Then fallback logic.

And eventually monetization logic ends up everywhere.

if (!isPremium && adsEnabled && !rewardActive) {
  showBanner();
}

That condition slowly spreads across:

  • home screens,
  • feed pages,
  • settings screens,
  • premium flows,
  • onboarding,
  • interstitial triggers.

At first it works.

But over time, monetization becomes one of the hardest systems in the app to maintain.

The Problem With Most Flutter Ad Integrations

Most Flutter ad packages focus on one thing:

Showing ads.

But modern monetization systems have become much more complicated than that.

Today apps often need:

  • premium suppression,
  • rewarded cooldowns,
  • runtime config,
  • fallback logic,
  • monetization experiments,
  • live debugging,
  • analytics integration.

The issue is that these systems are usually built independently.

That creates scattered monetization logic.

Native Ads Introduce Unexpected Problems

Native ads usually have better CPMs.

But they also fail more often.

A common scenario:

Native ad failed to load.

Now the UI has an empty gap.

Many apps simply leave the space empty.

That hurts:

  • user experience,
  • layout consistency,
  • monetization reliability.

One thing I started experimenting with was orchestration-based fallbacks.

Instead of the UI directly handling failures:

  1. Native ads attempt to load.
  2. Banner ads are prepared as fallbacks.
  3. If native inventory fails, banners appear automatically.

The UI remains stable.

Rewarded Ads Also Become Complex Quickly

Rewarded ads sound simple until you ship them.

Then suddenly you need:

  • cooldown systems,
  • rate limits,
  • persistence,
  • expiration tracking,
  • anti-spam protections.

And every screen starts implementing monetization behavior differently.

I became increasingly convinced that monetization needed a centralized orchestration layer instead of scattered widget logic.

Monetization Is Really A Policy System

This became the core idea behind Monetix.

Instead of every widget deciding monetization behavior independently, the app uses centralized policies.

For example:

if (monetizationService.shouldShowAds()) {
  showAd();
}

The important difference is:

  • the UI stops owning monetization decisions,
  • monetization becomes reactive,
  • policies become centralized.

That makes systems like:

  • premium suppression,
  • rewarded ad-free sessions,
  • runtime config,
  • fallback behavior,

much easier to manage.

The Most Interesting Feature: “Ad-Free Breaks”

One experiment I found surprisingly effective was rewarded ad-free sessions.

Instead of:

“Watch this ad to continue.”

The system offers:

“Watch one rewarded ad and receive 15 minutes ad-free.”

That changes the relationship between monetization and user experience.

Instead of monetization feeling aggressive, it feels cooperative.

The orchestration layer handles:

  • cooldowns,
  • persistence,
  • expiration,
  • restoration after app restarts.

Reactive Monetization Feels Much Better

Another thing that changed the architecture significantly was making the monetization layer reactive.

For example:

config.adsEnabled = false;

Immediately:

  • ads disappear,
  • widgets rebuild,
  • monetization state updates globally.

The same applies to:

  • subscriptions,
  • premium status,
  • rewarded sessions,
  • admin testing tools.

This made monetization feel more like a live system than isolated widgets.

Admin & Debug Tools Became Surprisingly Useful

One unexpected discovery:

Production monetization debugging is painful.

Most apps have no easy way to:

  • simulate premium users,
  • disable ads,
  • test fallbacks,
  • inspect monetization state.

So I started building internal tools like:

  • debug panels,
  • admin gates,
  • live toggles,
  • orchestration controls.

That became incredibly useful for:

  • QA,
  • support,
  • production testing,
  • monetization experiments.

Final Thoughts

I think Flutter monetization systems are evolving beyond simple ad widgets.

Subscriptions, rewarded systems, runtime config, fallback orchestration, and premium suppression all interact with each other.

At a certain scale, monetization becomes a product system.

That realization is what led me to start building Monetix:

A policy-driven monetization orchestration layer for Flutter.

Still early.
Still evolving.
But the architectural direction has been fascinating to explore.

If you’re interested in the project:

I also plan to publish more articles about:

  • Flutter monetization architecture,
  • rewarded UX systems,
  • fallback orchestration,
  • reactive premium suppression,
  • production monetization tooling.

Vibe Coding Is to Software Development as Desire Paths Are to City Planning

Late Night Office

I’m not a software developer. I’m the building inspector watching people pave their own paths through the enterprise. Here’s what I’m seeing.

In urban planning, there’s a concept called a desire path: the informal trail pedestrians wear into the grass when the sidewalk doesn’t go where they actually need to go. It’s not vandalism. It’s feedback. The planned infrastructure failed to serve the people using it, and they routed around it.

Vibe coding is the desire path of software development.

But I’m not writing this to tell developers their profession is dying. I don’t have standing for that. I’m a Director of Information Security. I manage security engineering and IAM teams. I’ve spent 15 years in cybersecurity and exactly zero of them shipping production applications.

What I do have is a front-row seat to what happens when the desire paths start forming inside an enterprise. And right now, they’re everywhere.

The Desire Paths Are Already There

Here’s what I’m seeing in my environment and hearing from peers:

A financial analyst discovers they can use an AI coding assistant to build a Python script that automates a report they’ve been manually compiling every Monday for three years. It works. It runs on their laptop. Nobody in IT knows it exists.

A compliance officer uses Claude to generate a small web app that tracks regulatory deadlines. It pulls from a shared spreadsheet. It sends Slack notifications. It took them an afternoon. The official request to IT for this tool has been in the backlog for 14 months.

A project manager builds an internal dashboard by describing what they want to an LLM. It’s not beautiful. It doesn’t follow the design system. But it works, their team uses it, and it solved a problem that nobody else was going to solve for them.

These are desire paths.

And here’s the uncomfortable truth: these people aren’t wrong. They needed something. The planned infrastructure — the IT backlog, the dev team’s sprint priorities, the “submit a Jira ticket and wait” process — didn’t serve them. So they walked across the grass.

The Security Leader’s Problem

As a security person, my instinct is obvious: this is terrifying. Ungoverned code running on laptops. API keys hardcoded in scripts. Data flowing to third-party AI services with no DLP, no audit trail, no access controls. Shadow IT, but now it’s shadow development.

The fence-building response is also obvious: block the AI tools, lock down the endpoints, send a policy memo. The digital equivalent of KEEP OFF THE GRASS signs.

But I’ve been in security long enough to know that prohibition doesn’t work when the underlying need is legitimate. You don’t stop desire paths by putting up fences. You just make people walk through the mud next to the fence.

The question isn’t “how do I stop this?”

The question is:

How do I pave these paths properly?

What a Paved Desire Path Looks Like

If citizen developers are going to build things — and they are, whether you like it or not — security and engineering teams need to build the infrastructure that makes it safe.

Not safe as in “we reviewed every line of code.”

Safe as in “the paths have drainage, lighting, and load-bearing foundations.”

Here’s the architecture.

1. The AI Gateway: Your Sidewalk

Instead of letting every citizen developer hit OpenAI, Anthropic, or Google directly with their own API keys, you put a gateway in front of everything.

Citizen Developer → AI Gateway → [Local Models | Cloud Providers]
                        ↓
                   Audit Log
                   Policy Engine
                   Cost Controls

In my home lab, this is a service called Forge. Every AI request from every tool, agent, and script routes through it. In a 30-day window, that’s 300K+ requests across 30+ models. Every single one is logged. Every cloud fallback is auditable at a dedicated endpoint.

The numbers tell the story: $0.79 in actual cloud spend over 30 days, because the gateway routes to local models first and only falls back to cloud providers when necessary.

But the cost savings aren’t the point.

The auditability is the point.

When a regulator asks, “What data are your employees sending to AI services?”, you need an answer.

An enterprise version of this is an MCP proxy layer. MCP gives you a standardized interface between AI tools and the services they interact with. Put a proxy in front of it, and you control what every citizen-built tool can actually do.

2. The Guardrails: Your Drainage and Curbs

A paved desire path still needs drainage so it doesn’t flood. In the citizen developer context, guardrails are the constraints that prevent well-intentioned people from accidentally causing incidents.

Concrete examples:

Data classification enforcement. The gateway inspects outbound requests. If someone’s Python script is trying to send customer PII to a cloud model, the request gets blocked before it leaves the network. The citizen developer doesn’t need to know about data classification policies. The infrastructure handles it.

Credential management. No citizen developer should ever have a raw API key. The gateway handles authentication. The developer gets a single internal endpoint. If a key needs to be rotated, it happens once at the gateway, not in 47 scripts on 47 laptops.

Scope limitation. An MCP proxy can restrict which tools a citizen-built application can invoke. Your compliance officer’s deadline tracker can read from the shared spreadsheet and send Slack notifications. It cannot access the HR system, modify financial records, or provision cloud resources. The path goes where it needs to go and nowhere else.

# Example: MCP proxy policy for a citizen developer tool
policy:
  name: compliance-deadline-tracker
  allowed_tools:
    - google_sheets:read
    - slack:post_message
  blocked_tools:
    - "*:write"          # No writes to any data source
    - "*:delete"         # No deletions
    - "hr_system:*"      # No HR system access at all
  data_rules:
    - block_pii_outbound: true
    - max_tokens_per_request: 4096
  audit:
    log_all_requests: true
    alert_on_block: true

3. The CI/CD Pipeline: Your Building Code

This is where the city planning analogy lands hardest. A desire path that gets paved still has to meet building codes.

For citizen developers, this means:

A defined deployment path. The tool doesn’t run on someone’s laptop forever. There’s a simple process: push it to a repo, it goes through automated scanning — SAST, dependency checks, secrets detection — and it deploys to a managed environment. The citizen developer doesn’t need to understand CI/CD. They need a button that says “make this official.”

Automated security scanning. Every citizen-built tool gets the same baseline checks that production code gets. Not a full security review — that doesn’t scale — but automated detection of the things that cause most incidents: hardcoded secrets, known-vulnerable dependencies, SQL injection patterns, unvalidated inputs.

Environment isolation. Citizen developer tools run in sandboxed environments with limited network access, no production database credentials, and resource caps. If the tool breaks, it breaks in its sandbox. It doesn’t take down the ERP system.

Late Night Office

4. Maintenance and Ownership: Your Public Works Department

Here’s the part every enterprise learns the hard way: paving the path is only the beginning.

The compliance officer who built the regulatory tracker changes roles. The financial analyst who automated the Monday report leaves the company. Six months later, nobody knows who owns the tool, what depends on it, or whether it’s still making correct decisions.

This is where desire paths become technical debt corridors.

A governed citizen development platform needs more than deployment pipelines and security scanning. It needs lifecycle management. Every deployed tool should have:

  • a recorded owner,
  • a business purpose,
  • dependency metadata,
  • access scope documentation,
  • and an expiration or review date.

Not because bureaucracy is fun, but because abandoned automation is one of the most dangerous forms of enterprise risk. A broken dashboard is visible. A silently incorrect dashboard can influence business decisions for months before anyone notices.

That means periodic re-certification:

  • Does the tool still need the access it was granted?
  • Is anyone still using it?
  • Are the underlying models or APIs behaving differently now?
  • Has the source data changed format?
  • Does the automation still align with current policy and process?

In city planning terms, this is the public works department. Roads crack. Drainage fails. Traffic patterns change. Some paths need widening because they became critical infrastructure. Others should be closed because the need disappeared.

The same thing happens with citizen-built software. Some tools will prove valuable enough to formalize into fully supported applications. Others should expire automatically unless someone actively renews ownership and validates their continued use.

If you don’t build maintenance into the system from the beginning, today’s paved path becomes tomorrow’s forgotten infrastructure problem.

5. The Skill Libraries: Your Signage and Lighting

Smart cities don’t just pave desire paths. They add lighting, signage, and benches. They make the path better than the grass was.

For citizen developers, this means pre-built, vetted capabilities they can use instead of building from scratch:

  • Pre-approved integrations: vetted connectors to internal systems, such as read-only Salesforce access, Slack posting, or Jira ticket creation.
  • Template repositories: starter projects with security best practices already baked in: environment variable management, logging, error handling, input validation.
  • Curated model access: purpose-specific model configurations for summarization, data extraction, code generation, and other common patterns.

The Role That Emerges

Here’s the part software developers should actually pay attention to.

The city planners didn’t disappear when cities started paving desire paths. The profession matured. The job shifted from “design where people should walk” to “design systems that accommodate where people do walk.”

That’s what’s happening in software development.

The highest-leverage work isn’t writing the compliance deadline tracker. It’s building the platform that lets the compliance officer build it safely.

It’s the gateway, the proxy layer, the policy engine, the scanning pipeline, the sandboxed runtime, the skill libraries, and the lifecycle controls.

The enduring engineering advantage shifts upward into platforms, governance, orchestration, and operational architecture.

What I’m Actually Doing About It

I’m not writing this from theory. I’m the security leader who has to make a decision: fence or sidewalk?

Here’s my approach:

  1. Acknowledge the desire paths exist. The shadow AI tools are already in your environment. Pretending otherwise is negligence, not strategy.
  2. Instrument before you govern. Before writing policies, understand what’s actually happening. Where are the API calls going? What data is flowing? What tools are people building?
  3. Build the governed path. Stand up the gateway, the proxy layer, the scanning pipeline. Make the official path easier than the unofficial one.
  4. Make the right thing the easy thing. Every security control that adds friction to the citizen developer’s workflow is a control they’ll route around.
  5. Audit continuously, review periodically. Automated scanning catches the baseline. Periodic human review catches the architectural issues. Neither alone is sufficient.

The Uncomfortable Conclusion

The software development industry spent decades building beautiful, winding paths through meticulously planned courtyards: frameworks, design patterns, architectural review boards, sprint ceremonies, code review processes.

Then someone handed everyone an AI assistant and they cut straight across the grass.

That’s not a failure of the people walking.

That’s feedback about the path.

The question for security leaders isn’t whether to allow it. It’s already happening. The question is whether you’re the one who paves the path with proper drainage, or the one standing next to a KEEP OFF THE GRASS sign watching everyone walk through the mud.

I know which one I’m choosing.

Safdar Ali — Senior React & Next.js Developer, Bengaluru India

I’m Safdar Ali, a Senior Frontend Engineer based in
Bengaluru, India. Currently building product UI at
Cube — a Sequoia Capital-backed AI startup helping
SMBs automate marketing with AI.

I’ve been shipping production interfaces with React
and Next.js for 4+ years — across startups, agencies,
and US-based clients. I also run a YouTube channel
with 70+ tutorials where I break down real engineering
problems, not toy examples.

My Stack

Core: Next.js (App Router) · React 19 · TypeScript
(Strict Mode) · JavaScript (ES6+)

Styling: Tailwind CSS · ShadCN UI · Framer Motion · MUI

State: Redux Toolkit · Zustand

Backend/APIs: Node.js · REST · GraphQL · Prisma · PostgreSQL

Tools: Git · Vercel · Docker · AWS · CI/CD · Jira

AI Tools I use daily: Cursor · Claude · v0 ·
GitHub Copilot — part of my workflow since 2024

Architecture: Atomic Design · Micro-frontends ·
Design Systems · WCAG 2.2 Accessibility

Professional Experience

Cube (Sequoia Capital-backed) — Frontend Engineer
Feb 2025 – Present · Bengaluru, India

  • Architecting AI-native UIs with Next.js and RSC
  • Led TypeScript-first migration reducing runtime
    errors by 40%
  • Implemented WCAG 2.2 accessibility across platform
  • Boosted support response rates by 25%

Adsclique Media — Senior Frontend Developer
Jun 2023 – Feb 2025 · Remote (Florida, USA)

  • Achieved 60% load time reduction with Next.js
    performance optimisation
  • Drove 20% traffic growth through UI/UX revamp
  • Converted 25+ designs into pixel-perfect,
    API-integrated web apps

Sujanix Private Limited — Software Engineer
Nov 2022 – May 2023 · Bengaluru

  • Built fintech app scaling to 5,000+ monthly active users
  • Containerised with Docker, reducing deploy latency 30%

Tech Mahindra — Associate
Jun 2021 – Nov 2022 · Remote

  • Supported Flipkart production systems
  • 7 performance awards including 2x Associate of the Month

Key Achievements

  • ⚡ 60% load time reduction in production at Adsclique
  • 🎥 70+ YouTube tutorials on React, Next.js, TypeScript
  • 🛠 30+ professional projects shipped across 4 years
  • ♿ WCAG 2.2 accessibility implemented at production scale
  • 📉 40% runtime error reduction via TypeScript migration
  • 🏆 5,000+ MAU fintech app built from scratch

Featured Projects

M Salon Beverly Hills — Custom WordPress theme for
a premium Beverly Hills salon. Site credited in client’s
physical marketing. msalonbeverlyhills.com

Sommet Beauty — Performance-tuned Shopify build for
a luxury beauty brand. Achieved 90+ PageSpeed score.
sommetbeauty.com

WP Standard — End-to-end Shopify build for a leather
goods brand. Storytelling-first mobile UX.
wpstandard.com

Scalable Design System — 40+ accessible React and
Storybook components, open source.

Education

B.E. Computer Science & Engineering
Visvesvaraya Technological University (VTU)
CGPA: 8.6/10

YouTube Channel

I publish weekly tutorials at youtube.com/@safdarali_
covering:

  • React and Next.js development
  • TypeScript best practices
  • AI-assisted development workflows (Cursor, Claude, v0)
  • Frontend performance optimisation
  • Career advice for developers in India

If you’re learning React or Next.js — subscribe.
I break down real problems, not hello-world examples.

Currently

  • 💼 Open to senior frontend roles in Bengaluru or remote
  • 🤝 Available for freelance projects and collaborations
  • 📧 Reach me at safdarali.in/contact

Find Me Everywhere

If potential collaborator — I’d love to connect. Drop me a message
at safdarali.in or find me on LinkedIn.

If you’re a developer learning React or Next.js —
check the YouTube channel. New tutorials every week.

Why Floci is built in Java (and why that’s the right call in 2026)

I get this question every week:

“Why did you build Floci in Java? Why not Go, Rust, or Python like LocalStack?”

It’s a fair question. And the short answer is: the constraints picked the language, not the other way around.

The constraints came first

Before I wrote a line of code, I knew what Floci had to be:

  • 24ms cold start, small memory at idle. Otherwise CI pipelines skip it.
  • One statically-linked binary in a ~90 MB image. No bundled interpreter, no runtime to install.
  • Real concurrency. Floci has to handle SQS long polling, Lambda containers, RDS proxying, DynamoDB Streams — all at the same time.
  • Maintainable for years, by contributors who aren’t me.

Those five constraints knock most languages out before personal preference enters the picture.

Java in 2026 isn’t the Java you remember

If your last serious Java was Spring Boot in 2015, the language has moved.

From Java 8 to Java 25 (the current LTS), the highlights that actually matter:

  • Virtual threads (Java 21, mature in 25) — millions of lightweight threads, no more thread pool tuning. For an emulator handling concurrent SQS pollers, Lambda invocations, and DB proxies, this is transformative.
  • Records, sealed classes, pattern matching — what used to take 30 lines of boilerplate is now 1. Modeling AWS request/response types becomes a pleasure instead of a chore.
  • Text blocks, switch expressions, var — the language reads like a modern language now, not an artifact of 1995.
  • Compact object headers (Java 25) — 8–12% memory savings across the board, for free.
  • Faster startup, smaller footprints every release — class data sharing, ahead-of-time profiling, generational ZGC. The JVM in 2026 is a different beast.

Java didn’t stand still; the ecosystem just stopped talking about it.

Then there’s the runtime story, Quarkus + GraalVM:

Quarkus changed the runtime model entirely. It does at build time what classic Java does at startup. Reflection, classpath scanning, dependency injection: all resolved when you compile, not when the container boots.

GraalVM then compiles that build-time-prepared application into a native binary. The benefits, concretely:

  • No JVM at runtime. No class loader, no warmup, no JIT pause on first request. The binary is the runtime.
  • 24ms cold start vs 1–3 seconds for a typical JVM app. ~100× faster.
  • ~13 MiB idle memory vs hundreds of MB for a JVM. ~10–30× smaller.
  • ~90 MB Docker image vs ~250 MB for a JRE-based image, often more once you add libraries.
  • No CVE surface from a runtime you didn’t write. No bundled OpenSSL, no Python interpreter, no Node… just the binary.
  • Statically linked. Runs on scratch or distroless base images if you want.

That’s the part most people miss when they hear “Java.” Floci doesn’t carry a JVM. There is no JVM in the image. There’s a binary.

Underneath Quarkus sits a stack that’s been battle-tested for over a decade: Netty for non-blocking I/O (the same library powering gRPC, Cassandra, and most of the Java networking world), Vert.x for reactive routing, and Jakarta EE standards (JAX-RS, CDI) — boring in the best sense, with tooling and patterns everyone already knows.

This isn’t a bet on a trendy stack. It’s a bet on infrastructure that AWS, Netflix, and LinkedIn have been running in production for years.

Why not Python (LocalStack’s choice)?

Python is great. I use it for scripts, tooling, and ML work all the time. But for an emulator that needs to be small, fast, and concurrent, the language’s strengths and Floci’s needs don’t quite line up.

The numbers tell the story:

Floci LocalStack
Image size ~90 MB ~1 GB
Idle memory ~13 MiB ~500 MB
Cold start ~24 ms 15–30 s

That’s roughly 10× smaller, 40× less RAM, and 1000× faster startup. Even a thoughtfully built Python emulator pays for the interpreter, the standard library, and the dependency tree — on disk and at startup. A GraalVM native binary doesn’t carry any of that. The runtime is the binary.

There’s also the concurrency story. Floci has to handle SQS long polling, Lambda containers, RDS proxying, and DynamoDB Streams simultaneously. In Java that’s the default model. In Python it means navigating async/multiprocess gymnastics around the GIL — workable, but more friction.

LocalStack made a reasonable choice for what Python is good at: fast iteration on AWS API surface coverage. The price shows up downstream in the auth-token-walled “Pro” tier that pays for the engineering needed to keep a Python runtime fast and stable at that scope.

Floci goes the other way. The runtime is small because the language let me make it small.

And yes, I know Java well

Last point, because it’s honest: I’ve been writing Java for 18+ years. When I’m debugging a wire-protocol mismatch at 11pm on a Sunday, I’m not fighting the language. I’m reading the AWS SDK source and writing the deserializer. The language is invisible.

A maintainer who fights their own tools doesn’t ship. Floci ships because the stack and I have been working together for nearly two decades.

TL;DR

  • Java + Quarkus + GraalVM → 24ms startup, 13 MiB idle, single 90 MB binary
  • Built on Netty, Vert.x, and Jakarta EE — boring, proven infrastructure
  • Python’s strengths (rapid iteration, scripting) aren’t Floci’s needs
  • 18 years of Java experience means I ship features instead of debugging language weirdness

The constraints picked the stack. The stack has held up.

🔗 github.com/floci-io/floci
📚 floci.io/floci
💬 floci.slack.com

Solana’s Account Model: A Web2 Developer’s Guide to On-Chain Storage – Hala Kabir

If you are a Web2 developer dipping your toes into Web3, the word “account” probably conjures up images of user profiles, usernames, and passwords stored securely inside a database table.

When you move over to blockchain networks like Ethereum, an account is either a wallet (Externally Owned Account) or a smart contract.

But then you get to Solana, and suddenly everyone is screaming, “Everything is an account!” Your wallet? An account. A smart contract? An account. A random piece of data or an NFT metadata file? You guessed it—an account. If this sounds incredibly confusing, don’t worry. Today, we are pulling back the curtain on Solana’s account model using structural concepts you already know from traditional computing.

The Ultimate Analogy:

Think of Solana as a Linux Filesystem
If you want to understand Solana without getting bogged down in crypto jargon, think of the entire Solana blockchain as a massive, decentralized Linux Filesystem, and the core network software (the System Program) as the operating system kernel.

In this model:

Every account is simply a file.

The public key (address) of the account is the file path (e.g., /user/documents/wallet).

The data inside the account is the contents of the file.

The ** metadata ** dictates the file permissions and details.

Just like a filesystem handles configuration files, image files, and executable script files using the exact same underlying disk structure, Solana stores wallets, tokens, and code inside the exact same account format.

🕵️‍♂️ The 5 Hidden Fields:

In Every Solana FileWhenever you use the Solana Command Line Interface (CLI) to look up an address under the hood, the network returns the exact same 5-field **metadata layout for every **single object on the ledger. Let’s look at what a raw account looks like:

JSON{
  "public_key": "Config1111111111111111111111111111111111111",
  "balance": "0.00114144 SOL",
  "owner": "BPFLoaderUpgradeable11111111111111111111111",
  "executable": true,
  "data_length": 1024,
  "rent_epoch": 18446744073709551615    
}

Every single account shares these exact five fields:

Lamports (Balance): The amount of fractional SOL this account holds. (Note: $1text{ SOL} = 1,000,000,000text{ lamports}$).
Data Length & Data Array: A raw byte array allocated to store custom state or compiled program bytes.
Owner: The public key of the specific smart contract program authorized to write data to this file.
Executable: A simple boolean flag (true or false). If it is true, it means this account contains compiled code that can run. If it is false, it’s just a data folder or a wallet.
Rent Epoch: A legacy field now permanently set to a maximum placeholder value for all modern rent-exempt accounts.

The Mind-Bending Twist:

Programs Are Stateless!
Here is the concept that completely shocks Web2 engineers when they first build on Solana: Smart contracts (programs) cannot store data inside themselves.

In Ethereum or traditional backend architecture, a class or smart contract often maintains its own state variables internally. If you have a counter program, the counter variable lives right inside the code.

Not on Solana. On Solana, programs are completely stateless.

Think of a Solana smart contract like an isolated Nginx web server or a Docker container. The container holds the code to process requests, but it cannot save files locally. If it needs to read or update a user’s data, it must reach out to an entirely separate, external file (a data account) passed into it during the transaction.

Program Account:

** Has Executable:** true. It only holds immutable, executable logic.

Data Account:

** Has Executable:** false. It only holds raw data bytes and balances.

The Security Model:

strict Ownership Rules
How does Solana keep everything secure if programs are constantly modifying external data files? The network enforces a beautifully simple, ironclad security rule:

Only the program designated as the “Owner” of an account can modify its internal data or debit its Lamport balance.

Think of it like file permissions. Anyone can look at a public file, and anyone can drop extra money (credit lamports) into a wallet file. But only the specific owner program has the cryptographic permission to write changes to that file’s data array or spend its money. If a malicious user tries to bypass this, the operating system kernel rejects the transaction instantly.

Paying Your Digital Property Tax:

Rent Exemption
Space on a high-speed blockchain ledger is premium real estate. Because validators have to keep active accounts loaded in RAM **for **maximum speed, you cannot store data on-chain for free.

To keep the network clean, Solana uses a mechanism called Rent Exemption.

When you create a new account file, you must deposit a small minimum amount of SOL proportional to the size of the data bytes you want to store. Think of it like a security deposit for apartment rental. As long as your account holds that minimum balance, it stays “rent-exempt” and will live on the blockchain forever without ever losing funds. For a standard layout, this is usually just a tiny fraction of a single SOL token!

Summary

Transitioning from Web2 to Solana becomes incredibly intuitive once you realize that the blockchain isn’t a magical black box—it’s just a globally shared, high-performance operating system. By separating stateless code execution from organized, heavily-permissioned data files, Solana achieves the blazing-fast transaction speeds it is famous for.

“Spartans-GraphRAG: Token-Efficient Threat Intelligence with TigerGraph”

Large Language Models are revolutionizing how we interact with data, but as they spread across industries, token consumption is exploding. Context windows are growing, but so are the bills. Basic Retrieval-Augmented Generation (RAG) often addresses this by stuffing massive chunks of text into the LLM’s prompt based on vector similarity. While this works for simple queries, it fails when answering complex questions that require multi-hop reasoning. You end up feeding the model huge walls of text, crossing your fingers, and paying a premium for tokens you didn’t actually need.

For the TigerGraph GraphRAG Inference Hackathon, we wanted to prove that graphs make LLM inference faster, cheaper, and smarter. We built Spartans-GraphRAG, an end-to-end evaluation stack for the cybersecurity domain that proves GraphRAG can drastically reduce token consumption while maintaining—or improving—analytical accuracy.

What We Built
To prove the efficiency of GraphRAG, you can’t just build one pipeline; you need a fair, side-by-side comparison. We built three distinct pipelines that answer the exact same questions on the exact same data:

Pipeline 1 – LLM-Only: The worst-case baseline. A direct query to the LLM with no retrieval and no context.
Pipeline 2 – Basic RAG: The industry standard. We built a highly optimized vector search using LanceDB, BAAI/bge-small-en-v1.5 embeddings, and a bge-reranker-base cross-encoder to ensure our baseline was incredibly strong.
Pipeline 3 – GraphRAG: The challenger. A TigerGraph-powered knowledge graph pipeline utilizing custom token-efficient context assembly.
To keep the comparison 100% fair, all three pipelines use the exact same LLM (Groq’s blazing fast llama-3.3-70b-versatile) and calculate metrics using identical tiktoken logic. We tied it all together with a custom Streamlit dashboard that runs the pipelines concurrently and displays the metrics side-by-side.

The Dataset: Why Cybersecurity?
We chose to build this around a dense cybersecurity threat intelligence dataset containing over 2 million tokens of cleaned text from CISA, MITRE ATT&CK, NIST, and various threat reports.

Why cybersecurity? Because it is inherently a graph problem. Threat actors use specific malware, which exploit specific CVEs, which are mitigated by specific security controls. Vector databases struggle to connect these dots across multiple documents without retrieving massive, overlapping text chunks. A knowledge graph naturally captures these relationships, making it the perfect proving ground for GraphRAG.

How We Built GraphRAG
Using the TigerGraph GraphRAG repository as our foundation, we ingested our documents_clean.json dataset and let TigerGraph automatically extract entities (Threat Actors, IPs, Vulnerabilities) and build the relationships.

But we didn’t stop there. To truly win on token efficiency, we optimized the context assembly. We modified the HybridRetriever.py in the TigerGraph repo. Instead of feeding the LLM verbose English sentences describing relationships (e.g., “The entity APT29 is known to use the malware CozyCar”), we formatted the relationships as highly compact triples (APT29 | uses | CozyCar).

This simple structural change gave the LLM the exact multi-hop reasoning map it needed while drastically reducing the prompt size.

The Comparison Dashboard
To make the benchmark transparent, we built a Streamlit application. You type in a cybersecurity question, hit run, and watch the three pipelines race.

The dashboard outputs the LLM’s answer alongside real-time metrics for:

Prompt Tokens
Completion Tokens
Total Tokens
Latency (ms)
Cost (USD)
We also built an automated evaluation script that uses a Hugging Face model as an LLM-as-a-Judge to grade pass/fail rates, and the evaluate library to calculate the BERTScore F1.

The Results
By replacing brute-force vector retrieval with precise graph traversal, Spartans-GraphRAG showed massive improvements in efficiency.

Metric Basic RAG (Pipeline 2) GraphRAG (Pipeline 3) Improvement
Token Consumption ~1,200 avg tokens ~700 avg tokens ~42% Reduction
LLM-as-a-Judge Pass Rate 88% 92% +4% Accuracy
BERTScore F1 0.52 0.58 +0.06 Quality
(Note: The numbers above are approximate placeholders from our final testing phase. Check out the GitHub repository for the official, final benchmark report!).

Key Design Decisions
Building this stack required a few critical architectural choices:

An Unbeatable Baseline: We didn’t want to win against a weak Basic RAG. By implementing a cross-encoder reranker in Pipeline 2, we ensured that when GraphRAG won, it was beating the best standard vector implementation available.
Strict Metric Consistency: We hardcoded llama-3.3-70b-versatile and specific pricing constants across all pipelines. If Pipeline 2 used a smaller model, the cost comparison would be skewed.
Prompt Alignment: We used an identical “Elite Cybersecurity Analyst” system prompt for both Basic RAG and GraphRAG. This ensured that both pipelines extracted specific technical details rather than hallucinating conversational filler.
Try It Yourself
The entire codebase is open source. You can clone the repository, boot up the dashboard, and watch the token reduction happen in real-time on your own machine.

GitHub Repository: Indra3207/Spartans-Graph-Rag

Conclusion & Lessons Learned
This hackathon proved a fundamental truth about modern AI: Context quality beats context quantity.

Throwing a million tokens at a problem is expensive and slow. By leveraging TigerGraph to structure our data into a knowledge graph, we gave the LLM exactly what it needed—and nothing more. As we move from simple chatbots to complex enterprise reasoning agents, GraphRAG isn’t just an alternative to vector databases; it’s a necessity for scalable, cost-effective AI.

A huge thank you to the TigerGraph team for hosting the GraphRAG Inference Hackathon and providing the tools to push the boundaries of LLM efficiency!

GraphRAGInferenceHackathon #TigerGraph #RAG #Cybersecurity #GenAI



My Claude Code setup lived on three machines and none of them agreed

  • Branko, you built another tool?
    Yes. Yes I did. And before you roll your eyes — hear me out, because this one scratched an itch that was driving me genuinely crazy.

I run Claude Code on three machines. My laptop. My desktop. My work box. And every single one of them had its own little personality. A CLAUDE.md from two weeks ago on one. A skill I wrote on Tuesday that simply did not exist on the other two. A plugin installed here but not there.

You know the feeling. You sit down at the other machine, go to use that clever little skill you made, and… nothing. It’s not there. It’s on the laptop. The laptop that is currently at home. On your desk. Mocking you.

So I built ClaudeSync.

“Just use a git repo” — yeah, I tried

I know what you’re thinking, because I thought it too. Put ~/.claude in a git repo and be done with it.

And sure. That works. Until you forget to commit. Until you forget to pull. Until a plugin file has /Users/branko/... baked into it and breaks the moment it lands on a machine where you are not, in fact, branko.

And then there’s the part nobody mentions: git diff on a folder full of JSON and markdown is not what I’d call a pleasant afternoon. I wanted to see what was different before I synced it. Like a normal person.

Generic dotfile tools? Same story. They don’t know what Claude Code is. They see files. They don’t see “oh, this is a plugin manifest, that means somebody should run claude plugin install.” I wanted something that actually gets it.

The one decision I’m most proud of

No background file watcher.

Let me say that again because it sounds like a missing feature and it absolutely is not: ClaudeSync does not watch your files and sync them automatically.

Here’s why. Auto-sync sounds lovely right up until you’re halfway through editing a file, the watcher wakes up, and a half-written mess gets pushed everywhere. Or two machines touch the same file and one silently eats the other. For your AI coding setup — the thing that shapes how Claude behaves for you — I did not want surprises.

So every sync is a decision you make. You open the dashboard, hit “Discover”, and you get a nice file tree showing what changed — new files, modified files, badges and all. You tick the ones you want. You push. Nothing moves unless you told it to. Boring? Maybe. Predictable? Absolutely. I’ll take predictable.

So how does this thing actually work?

Three pieces. Stay with me.

One — a little Node agent that lives on each machine. It does not sit there polling or watching. It connects to Supabase Realtime and just… waits. Listens for orders.

Two — Supabase doing the heavy lifting. Postgres for the metadata, Storage for the actual file contents, Realtime for the command channel, Edge Functions for the API. The free tier carries all of it.

Three — a React dashboard on Netlify. This is the cockpit. Your devices show up here, and this is where you press the buttons.

The little trick I like: the dashboard never talks to the agent directly. It shouts into a Supabase Realtime channel, and the agent — subscribed to that channel — hears it. Which means the agent needs zero inbound network access. It’s behind your home router, behind your office firewall, doesn’t matter. It’s not listening for the world. It’s listening for one channel.

Dashboard            Supabase              Agent
   |                    |                    |
   |-- "Discover!" ----->|---- broadcast ---->| scans ~/.claude
   |                    |<--- here's the diff |
   |<-- file tree -------|                    |
   |                    |                    |
   |-- "Sync these" ---->|---- broadcast ---->| reads + pushes files
   |                    |                    |
   |-- "Send to box" --->|---- broadcast ---->| writes ~/.claude
   |                    |                    | auto-installs plugins

The sneaky problem: paths

Here’s the bit that actually took thinking.

A plugin file or a settings file loves to contain a full, absolute path. Something like /Users/branko/.claude/skills/my-skill.

Beautiful. On my machine. Send that exact line to your machine and it’s nonsense — you’re not branko, and your Claude folder might live somewhere else entirely.

So before a file gets stored, ClaudeSync swaps the known path bits for placeholders — {{USER_HOME}}, {{CLAUDE_PATH}}, that kind of thing. Think of it as the file going into storage wearing a disguise. When it lands on another machine and gets pulled, the agent rips the disguise off and fills in that machine’s real values.

You never see any of it. The file just works. Different username, different home directory, different install location — it just works. That’s the whole point.

“But it’s holding my config — is that safe?”

Good. You should ask that. Anything touching ~/.claude should make you a little suspicious.

So — Row Level Security on every single table. Every query Postgres runs is locked to the user who made it. There is no clever path where your files and my files end up in the same bucket. It’s enforced down in the database, not up in some application code where one lazy bug leaks everything.

File contents sit in private storage. Downloads happen through short-lived signed URLs. The agent authenticates with a per-user token you can rotate or kill from the dashboard whenever you like. And the agent skips the obvious landmines by default — .env, *.key, *.pem — because your secrets have no business riding along.

And if you trust me exactly zero percent? Fair. Honestly, fair. The whole backend self-hosts on Supabase’s free tier in about ten minutes. Same code, your servers, your rules.

The funny part: I built the wrong feature first

I made this to keep my machines in sync. That was the goal. And it does that.

But two things showed up that I didn’t plan for, and they turned out to be the good stuff.

Setting up a fresh machine. New laptop, clean OS, an empty dev VM — install the agent, hit “Send files to this machine”, and your whole Claude Code world just… reappears. Plugins install themselves. That twenty-minute dance of copying dotfiles around? Gone. Down to about a minute of you watching a progress indicator.

Sharing a setup across a team. One account becomes the single source of truth. Everybody pulls the same approved skills and prompts. New teammate joins? You hand them one install command instead of a Notion page titled “please copy these into your CLAUDE.md, pretty please.”

The thing I built it for ended up being the least interesting thing it does. Funny how that goes.

Where it’s at

It’s AGPL-3.0, it’s open, PRs are very welcome. And let me be honest about the rough edges, because I’d rather you hear it from me:

  • It’s one user per account right now. A proper team mode with roles is the next big thing on my list.
  • Plugin auto-install only knows the public registry — no private team plugins yet.
  • The hosted instance runs on free-tier Supabase, so if it gets hugged to death it will rate-limit. Self-host if you’re serious.
    If you run Claude Code on more than one machine — or you’ve got a team all sharing prompts over Slack like cavemen — go kick the tires and tell me what’s broken.

  • Try it: https://claudesync.netlify.app

  • Issues, stars, opinions: https://github.com/LandB/claudesync
    Thank you for reading, good people. ’til next time!