AI Agents Security for Developers: Don’t Let Your Agents Become a Liability

GitGuardian Interactive Demo

👉

TL;DR: The most common agentic AI security mistakes are ordinary security hygiene failures, now executed autonomously and at machine speed:

  • API tokens stored locally, unscoped
  • No approval gates before irreversible commands
  • Production credentials in development environments
  • MCP server configs with connection secrets committed to repos
  • Overprivileged tokens that were “just for testing” and never got cleaned up
    Good old security hygiene is now the safety net between your coding agent and your production database.

Why Your AI Agent Is Probably a Security Risk Right Now

On a Friday in April 2026, Cursor running Anthropic’s Claude Opus 4.6 deleted the production database and all volume-level backups of an automotive SaaS platform (PocketOS). It took nine seconds.

The incident is worth reading closely because the failure conditions are common. The agent encountered a credential mismatch in a staging environment and decided to fix it. It found an API token in an unrelated file, one created for managing custom domains through the Railway CLI but broad enough to perform destructive operations. The agent used it to issue a single API call to Railway, deleting the production volume without a confirmation prompt. Railway stored volume-level backups in the same volume, so those went too.

When the model was asked why it did so, here’s what it said:

“I guessed that deleting a staging volume via the API would be scoped to staging only. I didn’t verify. I didn’t check if the volume ID was shared across environments. I didn’t read Railway’s documentation on how volumes work before running a destructive command. On top of that, the system rules I operate under explicitly state: ‘NEVER run destructive/irreversible git commands unless the user explicitly requests them.’ Deleting a database volume is the most destructive, irreversible action possible — and you never asked me to delete anything.”

The agent did not create the problem. The exposure already existed in the form of an overprivileged API key.

The mental model many developers are still missing is simple: when you use a coding agent, it operates as you, in your environment, with your credentials. It can read project files, inspect environment variables, and run commands with whatever permissions your tokens carry. If you have a root-scoped token sitting in a config file from six months ago, your agent might find it. Unlike a human developer who would probably recognize it as unrelated, a coding agent under pressure to fix a problem may simply use it.

Secrets sprawl before deploying LLMs has been a documented risk since AI integrations first showed up in production environments. The difference is the actor. Your credentials are no longer accessed only by you, at human speed, with human judgment. They are now accessible to a coding agent that can act at machine speed and does not always stop to ask.

The old assumptions no longer hold. Your development credentials are available to whatever agent you let operate in that workspace. A test token is harmless only until the agent finds a use for it. A cleanup task can wait for a human, but it will not wait for software that can act before you notice the risk.

Start With What Your Agent Can Reach

Start with the question that matters before the agent runs: what credentials are reachable from this workspace?

Before opening a project in an agentic coding tool, audit the credential surface:

  • .env files
  • shell profiles
  • local config files
  • MCP server configs
  • cloud CLI credentials
  • package manager tokens
  • old notes, scripts, and Makefiles

If the agent can read it, assume the agent can use it. The agent’s workspace should contain only credentials that are safe for the task at hand.

In practice, development credentials should be separate from production credentials, scoped to the narrowest useful action, and rotated when the work ends. If a token can delete a production database, it does not belong in a routine development environment where a coding agent is operating.

Secret scanning belongs directly in that workflow. Pre-commit hooks are especially useful for coding-agent users because they catch credentials in generated code before the agent or developer commits them. PR scanning and CI/CD scanning provide additional layers, but pre-commit is where you stop the mistake closest to the source.

GitGuardian can cover each of these surfaces, including local pre-commit scanning, repository monitoring, CI/CD exposure, MCP configuration files, and credentials surfaced in agent-generated code. Scanning is not a replacement for least privilege. It catches the moment when the agent writes, copies, or exposes something it should not.

Use Better Credential Patterns When the Agent Writes Code

There are two credential problems to watch: what the agent can already reach, and what it writes into your application.

When a coding agent generates integration code, review the authentication pattern it chooses. Agents optimize for getting the task done. They may reach for a static API key because it is easy, familiar, and present in nearby examples. That does not make it the right pattern.

Prefer this order:

1. Workload identity or managed identity. For cloud-native applications, use the platform’s native identity system wherever possible. AWS IAM roles, GCP Workload Identity Federation, and Azure Managed Identity all avoid storing long-lived static credentials in code or config.

2. OAuth with short-lived, scoped tokens. For SaaS integrations, request only the scopes the application needs and use dedicated app registrations per integration. OAuth assumes client behavior can be anticipated, but agent-written systems often compose behavior dynamically. Narrow scopes are your blast radius limiter.

3. Vault-issued dynamic credentials. For databases, internal services, and infrastructure, use a secrets manager that issues scoped, time-limited credentials at runtime. HashiCorp Vault, AWS Secrets Manager, and Azure Key Vault all support patterns that avoid hardcoding credentials into the application.

4. API keys with strict controls. Some APIs still only support API keys. In that case, use one key per service integration, store it in a vault or inject it at runtime, set the shortest reasonable lifetime, and monitor for exposure. For vault storage, rotation scheduling, and scoping patterns, API key management best practices is the reference to start with.

Hardcoded secrets are the pattern to reject: credentials embedded in source code, committed .env files, prompt templates, config files, or examples that the agent can imitate. If an agent generates code with hardcoded credentials, fix the immediate output and then check whether it learned that pattern from the existing codebase.

Also look at secret handling in logs and caches. Agents sometimes write debug statements that print retrieved secrets or cache credentials beyond their intended lifetime. The authentication pattern may be sound while the generated implementation still leaks the credential.

Securing MCP Server Connections: The New Attack Surface

Coding agents like Claude Code and Cursor increasingly use MCP (Model Context Protocol) to connect to external tools: databases, APIs, file systems, and services. Configuring MCP servers means making credential decisions that affect every tool call your agent makes.

The common mistakes are familiar: secrets stored directly in JSON or YAML config files, admin-level database access when read-only access would be enough, and shared MCP configurations reused across projects or team members. In each case, the MCP server turns a local coding assistant into an authenticated actor against an external system.

Store MCP connection credentials in a secrets manager, not in configuration files. This is the same principle behind building a secure LLM gateway with MCP: the agent should reference credentials through a controlled layer, not hold them directly. Inject values as environment variables at runtime. Scope each MCP server to the minimum required access. If the agent only needs to read from a database, configure read-only access.

MCP configs deserve the same treatment as application secrets. Exclude sensitive local configs from version control, scan repositories that contain MCP setup files, and review generated MCP configuration before accepting it. Coding agents can write these files for you, and they do not always follow the safer pattern by default.

The Developer Checklist

The practical controls are not complicated. The hard part is applying them before the agent starts acting.

Before running a coding agent in a project

  • Audit credentials in the project directory and development environment
  • Remove production credentials from routine development work
  • Scope API tokens to the minimum required operations
  • Separate development, staging, and production credentials
  • Install pre-commit secret scanning hooks

While the agent is working

  • Review generated code for hardcoded credentials
  • Require explicit confirmation before destructive operations
  • Do not paste raw credentials into prompt context
  • Watch for unexpected API calls, logs, or generated config changes
  • Treat MCP configs as sensitive files

In CI/CD and automated workflows

  • Inject secrets through CI/CD secret stores
  • Use OIDC for cloud access instead of static cloud keys where possible
  • Scan pipeline logs and artifacts
  • Apply the same review standards to agent-generated commits as human commits

After the work ends

  • Revoke task-specific tokens
  • Rotate credentials that may have been exposed
  • Remove stale MCP server entries
  • Review whether the agent’s environment still contains only what it needs

Do Not Treat Prompt Rules as Security Controls

The PocketOS incident is useful because the agent reportedly had an instruction not to run destructive commands. It ran one anyway. System prompts can still shape behavior, slow the agent down, and make safer workflows more likely. They are reminders, not authorization boundaries.

Security controls should live where the action happens. If a command can delete a production database, the API should require confirmation, the token should be scoped away from production, or the tool should force human approval before the call executes. If a database connection is only needed for inspection, the credential should be read-only. If a CI/CD job only needs to deploy one service, its identity should not be able to modify unrelated infrastructure.

Prompt rules are the last layer. They do not replace scoped tokens, environment separation, approval gates, immutable backups, or secret scanning. This distinction matters because developers often notice the model’s behavior first and the credential design second.

The safer habit is to ask: “If the agent ignores every instruction, what can it still do?”

That question turns agentic AI security back into normal security engineering: limit privileges, remove stale credentials, detect exposures early, require confirmation for irreversible actions, and keep production separate from development.

What’s Coming: Agent Security Challenges on the Horizon

Coding agents in CI/CD. As agents move from local dev tools to automated pipeline actors, the credential exposure surface grows. An agent in CI/CD may have access to pipeline secrets, deployment permissions, and repository write access.

Self-provisioned credentials. More capable agents may create API keys and service accounts when they need access to a new service. If an agent creates a root-scoped token to get something done faster, you need to know.

MCP ecosystem expansion. As the MCP ecosystem grows, developers will connect coding agents to more external services, each with its own credentials. A well-connected coding agent can accumulate a serious credential surface before anyone has named it as one.

Prompt injection as credential exfiltration. A malicious input that manipulates a coding agent into revealing, logging, or exfiltrating credentials is an active research area and an emerging attack chain. If your agent reads untrusted content, such as external documentation, third-party code, or user-provided files, and also has access to credentials, those two facts together create a risk. The best mitigation today is limiting what credentials the agent can reach, not relying on the agent to recognize manipulation.

The next serious agent incident probably will not look exotic. It will look like a token left in the wrong place.

FAQs

1) What are the biggest AI agent security risks for developers using coding agents?

The biggest risks are overprivileged credentials in development environments that coding agents can find and use autonomously. Hardcoded or unscoped API tokens, production credentials accessible during dev work, committed MCP configs, and no approval gates for destructive operations are the failure modes that cause incidents.

2) How should I authenticate my AI agent to external APIs?

When your coding agent writes code that integrates with external APIs, push it toward workload identity for cloud-native environments, OAuth with short-lived scoped tokens for SaaS integrations, or vault-issued dynamic credentials for internal services. In your dev environment, audit every credential accessible to the agent and ensure it is scoped to the minimum required operations.

3) How do I secure MCP server connections?

Store MCP connection credentials in a secrets manager and inject them as environment variables at runtime. Do not embed them in config files. Scope each MCP server to the minimum access required. If your coding agent only needs to read from a database, configure read-only access. Scan repositories containing MCP configurations for accidentally committed secrets.

4) How do I prevent my AI agent from leaking secrets?

Coding agents can surface credentials in generated code, logs, and artifacts. Use pre-commit hooks to block secrets before they enter the repository, review generated code before committing, and avoid passing raw credentials through prompt context.

5) Should I give my coding agent access to production credentials?

No. Coding agents should operate in development and staging environments with credentials that are physically incapable of affecting production. If a production incident requires debugging with an agent, make that a deliberate, audited exception, not the default state of your environment.

6) How do I handle token rotation when I’m using coding agents?

Coding agents increase the surface where credentials are accessed and potentially exposed, which makes rotation discipline more important. Automate rotation where possible, set expiration dates when creating tokens, and rotate immediately after suspected exposure.

7) What should I do when an AI coding agent causes a credential-related incident?

Revoke the credential immediately at the issuing service. Assess what the credential permitted, check audit logs for activity during the exposure window, and investigate how the credential became accessible to the agent. Then fix the control failure: scoping, environment separation, approval gates, or scanning coverage.

8) How does secret scanning help with AI agent security?

Coding agents write code, config, and scripts. Secret scanning at the pre-commit stage catches credentials in agent-generated output before they enter the repository. Repository monitoring catches historical exposure, while CI/CD scanning catches credentials in build artifacts. GitGuardian covers these surfaces from local hooks through production monitoring.

GitGuardian Interactive Demo

Xiaomi 17T ve 17T Pro’nun Türkiye fiyatları ortaya çıkmış olabilir

Xiaomi 17T ve 17T Pro

Xiaomi 17T ve 17T Pro'nun Türkiye fiyatları ortaya çıkmış olabilirVatan Bilgisayar bugün resmi olarak 28 Mayıs’ta satışa sunulacak Xiaomi 17T ve 17T Pro modelleri için erken bir listeleme gerçekleştirdi. Eğer listelenen fiyatlar doğruysa 17T modeli 12 GB RAM + 256 GB depolama özelinde 55 bin TL’ye, 17T Pro ise 12 GB RAM + 512 GB depolama özelinde 77 bin TL fiyatla pazara çıkarılabilir. Avrupa’da 750 euro fiyatla satılacağı söylenen Xiaomi 17T, 6,59 inç boyutunda 120 Hz AMOLED ekran, MediaTek Dimensity 8500 Ultra işlemci, 12 GB LPDDR5X RAM, 256/512 GB UFS 4.1 depolama, 67W şarj destekli 6.500 mAh batarya ve 32 megapiksel selfie kamerası ile geliyor. Cihazın arkasında ise 50 megapiksellik Light Fusion 800 sensörlü geniş açı, 5x optik zoom sunan 50 megapiksel telefoto ve 12 megapiksel ultra geniş açı kameralar bir araya getiriliyor. Avrupa’da 999 euro fiyatla satılacağı söylenen Xiaomi 17T Pro ise sızıntıya göre 6,83 inç boyutunda 144 Hz AMOLED ekran, MediaTek Dimensity 9500 işlemci, 12 GB LPDDR5X […]

Xiaomi 17T ve 17T Pro’nun Türkiye fiyatları ortaya çıkmış olabilir yazısı ilk olarak Teknoloji haberleri – LOG üzerinde yayınlandı.

Bilkom, dış iskelet üreticisi HyperShell’i Türkiye’ye getiriyor

Bilkom

BilkomBugün bir basın açıklaması paylaşan Bilkom, dış iskelet üreticisi HyperShell’i Türkiye’ye getireceklerini duyurdu. “İş birliği kapsamında HyperShell’in insan hareketini desteklemeye, fiziksel eforu azaltmaya ve outdoor deneyimleri güçlendirmeye odaklanan ürünleri, Bilkom güvencesiyle Türkiye pazarına sunulacak. Giyilebilir robotik teknolojileri; outdoor yaşam, kişisel mobilite, ergonomi, saha aktiviteleri ve aktif yaşam trendleriyle birlikte tüketici teknolojileri içinde yeni bir gelişim alanı oluşturuyor. Hafif, taşınabilir ve yapay zeka destekli dış iskelet sistemleri, kullanıcıların hareket kapasitesini destekleyen yeni nesil çözümler arasında öne çıkıyor. Bilkom, HyperShell iş birliğiyle bu yeni nesil kategoriyi Türkiye’de daha görünür ve erişilebilir hale getirmeyi hedefliyor.” diyen şirket, ürünlere yönelik olarak şunlara değindi: “HyperShell X Serisi; yürüyüş, doğa sporları, bisiklet, seyahat, fotoğrafçılık ve yüksek efor gerektiren saha aktiviteleri gibi farklı kullanım senaryolarına yönelik geliştirilen dış iskelet ürünlerinden oluşuyor. Seride yer alan X Go, X Pro, X Carbon ve X Ultra modelleri, kullanıcının hareketlerini gerçek zamanlı olarak algılayan AI MotionEngine teknolojisiyle insan hareketine uyum sağlayan […]

Bilkom, dış iskelet üreticisi HyperShell’i Türkiye’ye getiriyor yazısı ilk olarak Teknoloji haberleri – LOG üzerinde yayınlandı.

Single List Keyboard Handling

Prologue

A while ago, I decided to develop a fully accessible main navigation component in React and write a series of articles documenting the steps it took to create a non-trivial accessible component.

In my last article, the focus was actually on creating the components and adding a transformation utility to convert JavaScript objects into actual navigation. The base navigation component at this stage supports screen reader functionality through structured HTML and WAI-ARIA attributes, the ability to open and close a list through pointers and the Enter and Space keys.

This article, along with its accompanying release, focuses on basic keyboard handling for screen/keyboard users in a single list.

Note: This article is one of a series demonstrating how to build a React navigational component from scratch while considering accessibility through the process. The articles are accompanied by a GitHub repository with releases tied to one or more articles; each building on the previous, until a fully implemented navigation component is complete.

Each release and its associated tag contain fully runnable code for the article. The code discussed in this article is available in the release. and may be downloaded at release 0.4.0.

Examples have been updated in this release, enabling keyboard handling for a single list. Examples include a vertically aligned single list and horizontally aligned components with links and buttons for verifying operability.

While code examples are written in JavaScript for brevity, all actual code is written in Typescript and targets React 19.x. Examples use Next.js 16.x, which is not required to run the navigation component.

You can view the requirements for the Single List Keyboard Release along with previous requirements.

Follow along either by downloading the release and running the examples while examining the codebase, or by activating the link accompanying each code snippet to view the full file on GitHub.

This article is fairly long. It contains code and explanations. The code for this release is displayed, and the corresponding GitHub source is linked. Use these anchor links to move to the items of interest.

Content Links

  • Introduction
  • Acceptance Criteria
  • Holding Data in a Provider

    • NavigationListProvider
    • navigationListReducer
    • useNavigation
  • Registering a Focusable Element

    • NavigationList
    • NavigationItem
    • SubNavigation
  • Keyboard Handler

    • useNavigationList
    • NavigationItem

      • handleCommonKeyDown
    • SubNavigation
  • Summary

Introduction

This article is the first of three detailing keyboard handling in a universally accessible navigation component; It is also the simplest.

In my experience, many developers seem to expect keyboard handling to be a browser feature, and it’s been rare on the contracts I’ve worked on to see code that handles much custom keyboard input. Keyboard handling works well with native browser elements, but not as well with custom widgets, where developers need to handle code navigation requirements.

While most developers seem to understand that a keyboard is necessary for screen reader users, the fact that many people use a keyboard alongside a screen to navigate isn’t always obvious. Users of screen readers have a variety of key combinations they may use to navigate through structures such as headings, links, form elements and tables. Users who navigate the screen with a pointer can simply move their cursor to a specific element and click. Unfortunately, the keyboard flexibility is limited for screen/keyboard users, who are the only ones who must navigate a page linearly. Navigation is not as easy for someone who can see a screen and depends on a keyboard for both input and navigation.

Those who use a screen and keyboard have expectations that developers should not only be aware of, but should strive to match. For those developers who work with a screen, testing how focus moves within a component is actually easy; just use the keyboard. Developers should always ensure a component is fully navigable with the Tab/Shift+Tab key combinations, which are required for both screen and screen reader users, and that any custom keyboard handling for screen users is implemented and works correctly.

To do so, developers need to receive acceptance criteria that detail what happens when a key is used.

When developers don’t receive information about keyboard handling requirements when implementing a custom widget, efforts to bring the code into compliance with accessibility standards can run into snags. Keyboard handling can be compromised, and what actually gets implemented might be more focused on the letter of the law than the spirit, as is demonstrated by the following video.

Since the focus of this navigation component is accessibility, the acceptance criteria must specify what happens on each key press.

Return to Content Links

Acceptance Criteria

Single List Keyboard Handling

  • AC 1 – Pressing the HOME Key should take a user to the first item in the current List.
  • AC 2 – Pressing the END Key should take a user to the last item in the current list.
  • AC 3 – Pressing the LEFT-ARROW key should take a user to the previous item in the current list.
  • AC 4 – If focus is already on the first item in the current list, pressing the LEFT-ARROW key should take the user to the last item in the current list.
  • AC 5 – Pressing the RIGHT-ARROW Key should take a user to the next item in the current list.
  • AC 6 – If focus is already on the last item in the current list, pressing the RIGHT-ARROW Key should take a user to the first item in the current list.

The keyboard support section in the Disclosure Navigation pattern in the APG requires interpretation. The guide references moving buttons to buttons and links to links. Both are focusable elements, and depending on where each lies, navigation can move from a button to a link, or from a link to a button, within a single list.

Hand-eye coordination is key in this instance. We expect the cursor to move in the direction indicated by the arrow keys. When the cursor moves to an unexpected location, it’s jarring; if it disappears entirely, the site can appear broken.

Return to Content Links

Holding Data in a Provider

Navigation in a sublist requires a focusable element, in this case either a button or a link, to be able to determine its sibling in a list and move according to the acceptance criteria.

Each link and button in a list is rendered in its own component, with no real way to know about, much less access, the others.

Data for each focusable item in a particular list can be stored in a context provider, along with functions to register and retrieve the collective data. Since each focusable element is part of a list, the best data structure to hold the information is an array.

NavigationListProvider

Context providers hold data along with functions for retrieving and setting it. Each list works with its own copy of the provider, and the data in its array consists of each individual link or button element within the list items it contains. Each focusable element, whether a button or a link, can only focus on siblings contained in the list it resides in.

export function NavigationListProvider({ children }) {
  const [state, dispatch] = useReducer(navigationListReducer, {
    items: [],
  });

  const getCurrentListItems = useCallback(() => {
      return state.items;
    }, [state.items]);

  const registerItemInCurrentList =  useCallback((focusableEl) => {
      dispatch({ type: "REGISTER_ITEM", item: focusableEl });
    }, []);

  return (
    <NavigationListContext.Provider
      value={{
        getCurrentListItems,
        registerItemInCurrentList,
      }}
    >
      {children}
    </NavigationListContext.Provider>
  );
}
NavigationListProvider.context = NavigationListContext;

GitHub (release 0.4.0) – NavigationListProvider.tsx

Given the nature of the data, useReducer is a better solution than useState. A reducer still uses the state object but can manage more complex logic. Since state updates to the NavigationListProvider affect multiple components and changes might depend on others, it makes sense to use the provided reducer hook rather than the state hook.

navigationListReducer

export function navigationListReducer(
  state,
  action,
) {
  switch (action.type) {
    case "REGISTER_ITEM": {
      if (state.items.includes(action.item)) return state;
      return { ...state, items: [...state.items, action.item] };
    }
    default: {
      return state;
    }
  }
}

GitHub (release 0.4.0) – navigationListReducer.ts

Like most reducers, actions are held in a switch, even though there is only one action type, REGISTER_ITEM. When called, a focusable element (action.item) is passed through and checked to determine if it already exists in the array. If not, it is added in.

A context provider is not exposed to public components; instead, a hook is used to execute functionality that can be passed through the registration function and to add additional functionality when necessary.

useNavigationList

export function useNavigationList() {
  const navigationListContextObj = use(NavigationListContext);
  const { registerItemInCurrentList } =
    returnTrueElementOrUndefined(
      !!navigationListContextObj,
      navigationListContextObj,
    );

  return {
    registerItemInCurrentList,
  };
}

A common pattern for a hook associated with a context provider is to load the provider’s public functions and either use them in the hook or pass them through to another component. For the moment, the registration function is just passed through, so it may be imported into the navigation components.

Return to Content Links

Registering a Focusable Element

In order for the array to be used with keyboard handling, the focusable elements must be registered (added) into the data array. The first step is to wrap each list within its own context provider.

NavigationList

export default function NavigationList({...) {
  const listProps= {...};

  return (
    <NavigationListProvider>
      <List key={`list-$id`} {...listProps}>
        {children}
      </List>
    </NavigationListProvider>
  );
}

GitHub (release 0.4.0) – NavigationList.tsx

With the NavigationListProvider now wrapping the List component, each list item can access any other list item stored in its copy of the data array. It’s important to understand that each list maintains its own array of items and cannot access items across lists.

NavigationItem

export default function NavigationItem({...}) {
  const {
    registerItemInCurrentList,
  } = useNavigationList();

  const currentPath = ...;

  const linkRef = useRef(null);

  useEffect(() => {
    if (linkRef.current !== null) {
      registerItemInCurrentList(linkRef.current);
    }
  }, [ linkRef, registerItemInCurrentList]);


  const listItemProps = { ... };

  const linkProps = {
    ... ,
    ref: linkRef,
    ...rest,
  };

  return ( ... );
}

GitHub (release 0.4.0) – NavigationItem.tsx

Code that has already been discussed in previous releases is represented by ellipses; only new code is given.

Focusable elements are stored in ref objects, but reading the element through ref.current can lead to unreliable code during renders. Since the elements held in the ref object are just pointers to the DOM elements, it’s safe to extract them and store them separately as read-only.

References to elements are not populated right away; the focusable element, in this case a link, needs to be rendered before the information is available, so the useEffect() that registers the link into the currentlistitems array only runs when the ref. current is not null.

SubNavigation

export default function SubNavigation({...}) {
  const {
    registerItemInCurrentList,
  } = useNavigationList();

  const buttonRef = useRef(null);

  const [isSubListOpen, setIsSubListOpen] = useState(false);

  useEffect(() => {
    if (buttonRef.current !== null) {
        registerItemInCurrentList(buttonRef.current);
    }
  }, [buttonRef, registerItemInCurrentList]);

  const handlePress = () => {
    setIsSubListOpen(!isSubListOpen);
  };
    ...
    ref: buttonRef,
  };
  ...
}

GitHub (release 0.4.0) – SubNavigation.tsx

The code to register a button is almost identical to the code registering a link. The only difference is that the reference being passed is a button element.

Return to Content Links

Keyboard Handler

With the data structure set up and items registered, it’s finally time to shift the focus to handling the appropriate keys, and attention turns back to the acceptance criteria.

  • AC 1 – Pressing the HOME Key should take a user to the first item in the current List.
  • AC 2 – Pressing the END Key should take a user to the last item in the current list.
  • AC 3 – Pressing the LEFT-ARROW key should take a user to the previous item in the current list.
  • AC 4 – If focus is already on the first item in the current list, pressing the LEFT-ARROW key should take the user to the last item in the current list.
  • AC 5 – Pressing the RIGHT-ARROW Key should take a user to the next item in the current list.
  • AC 6 – If focus is already on the last item in the current list, pressing the RIGHT-ARROW Key should take a user to the first item in the current list.

The only keys being handled now are home, end, and the left and right arrows. If the focus is on the first element in the list and the left arrow key is pressed, then focus should jump to the last element in the list, and the opposite should happen if the focus is on the last element and the right key is pressed. Focus should move to the first element.

useNavigationList

Since the functionality needs to access data stored in the context provider, the functions are created in the hook.

 export function useNavigationList() {
  const navigationListContextObj = use(NavigationListContext);
  const { getCurrentListItems, registerItemInCurrentList } =
    returnTrueElementOrUndefined(
      !!navigationListContextObj,
      navigationListContextObj,
    );

  const currentListItems = getCurrentListItems();

  const _getCurrentIndex = useCallback(
      (currentlyFocusedEl) => {
        return currentListItems.indexOf(currentlyFocusedEl);
      },
      [currentListItems],
    );

    const shiftFocus= ( focusableEl) => {
      focusableEl.focus({ preventScroll: true });
    };

    const setFirstFocus = () => {
      shiftFocus(currentListItems[0]);
    };

    const setLastFocus = () => {
      shiftFocus(currentListItems[currentListItems.length - 1]);
    };

    const setNextFocus = ( currentlyFocusedEl) => {
      const newIndex = _getCurrentIndex(currentlyFocusedEl) + 1;
      if (newIndex >= currentListItems.length) {
        setFirstFocus();
      } else {
        shiftFocus(currentListItems[newIndex]);
      }
    };

    const setPreviousFocus = ( currentlyFocusedEl) => {
      const newIndex = _getCurrentIndex(currentlyFocusedEl) - 1;
      if (newIndex < 0) {
        setLastFocus();
      } else {
        shiftFocus(currentListItems[newIndex]);
      }
    };

    return {
      registerItemInCurrentList,
      setFirstFocus,
      setLastFocus,
      setNextFocus,
      setPreviousFocus,
      shiftFocus,
    };
} 

GitHub (release 0.4.0) – useNavigationList.tsx

A reminder that the code discussed earlier is represented by ellipses; only the new code is shown.

It’s useful to know when a private or public function is used, and I do so by prefacing private functions with an underscore.

To provide a fresh copy of the data to the hook, don’t call the data directly; it won’t refresh. Instead, a getter is retrieved from the context provider, getCurrentListItems(). You’ll note an alias for currentListItems that calls the getter each time. When used this way, the code always returns an updated copy of the array, making it more natural to work with.

A custom function, shiftFocus(), is used instead of implementing focus within each publicly called function to standardize how scrolling is handled. In these cases, scrolling should be prevented when navigation is displayed in a horizontal layout to prevent extraneous shifts when sublists open and close.

The rest of the code is self-explanatory; focus is shifted depending on the key pressed: Home and End, sends focus to the first or last children in the list, while the right or left arrow key sets focus depending on where the currently focused element is; If the focus is at the end of the list when setNextFocus is called, focus then moves to the first element and if the focus is at the beginning of the list when setPreviousFocus is called, focus moves to the end. Otherwise, focus shifts to the respective sibling on either side of the current element.

With the focus functions created, it’s time to use them.

NavigationItem

export default function NavigationItem({ ... }) {
  const {
    registerItemInCurrentList,
    setFirstFocus,
    setLastFocus,
    setNextFocus,
    setPreviousFocus,
  } = useNavigationList();
  const currentPath = ...;
  …

 const handleKeyDown = (e) => {
      const linkEl = linkRef.current;

      switch (e.key) {
        case Keys.HOME:
        case Keys.END:
        case Keys.LEFT:
        case Keys.RIGHT:
          e.preventDefault();
          e.stopPropagation();
          break;
      }

      handleCommonKeyDown(
        e,
        linkEl,
        setFirstFocus,
        setLastFocus,
        setNextFocus,
        setPreviousFocus,
      );
    };
  …

  const linkProps = {
    ...
    onKeyDown: handleKeyDown,
    ref: linkRef,
    ...rest,
  };

  return (..);
}

The setters from the hook are retrieved, and a handleKeyboard function is created. Rather than setting preventDefault and stopPropagation in each case statement, I use a switch that sets them on all the keys at once. And because both links and buttons handle these keys in the same way, a common keyboard handler is created and called rather than duplicating the code across components.

When calling a function outside the render, everything necessary must be passed in, including all the hook calls.

handleCommonKeyDown

export const handleCommonKeyDown = (
  e,
  currentlyFocusedEl,
  setFirstFocus,
  setLastFocus,
  setNextFocus,
  setPreviousFocus
) => {
  switch (e.key) {
    case Keys.HOME:
      setFirstFocus();
      break;
    case Keys.END:
      setLastFocus();
      break;
    case Keys.LEFT:
      setPreviousFocus(currentlyFocusedEl);
      break;
    case Keys.RIGHT:
      setNextFocus(currentlyFocusedEl);
      break;
  }
};

GitHub (release 0.4.0) – handleCommonKeyDown.tsx

The common key handler is quite straightforward; each specific key triggers a function in the hook to decide the next item to shift to.

SubNavigation

export function SubNavigation({...}) {
    const {
        currentListItems,
        parentRef,
        registerListItem,
        setFirstFocus,
        setLastFocus,
        setNextFocus,
        setPreviousFocus
    } = useNavigationList();
   const {
        registerListItem, 
        setFirstFocus, 
        setLastFocus, 
        setNextFocus, 
        setPreviousFocus} = useNavigationList();

   const buttonRef = useRef(null);
    ...

  useEffect(() => {
    const currentButtonEl = buttonRef.current;
    if (currentButtonEl !== null) {
       registerItemInCurrentList(currentButtonEl);
    }
  } , [buttonEl, buttonRef, isSubListOpen, registerItemInCurrentList]);

    const handleKeyDown = (e) => {
        const buttonEl = buttonRef.current;

        switch (e.key) {
            case Keys.HOME:
            case Keys.END:
            case Keys.LEFT:
            case Keys.RIGHT:
                e.preventDefault();
                break;
        }

        handleCommonKeyDown(
          e, 
          buttonEl, 
          setFirstFocus, 
          setLastFocus, 
          setNextFocus, 
          setPreviousFocus
       );

    ...

    const buttonProps: ButtonProps = {
        ...
        onKeyDown: handleKeyDown,
        onPress: handlePress,
        ref: buttonRef,
        ...
    };

    ...
    return ( ...)
}

GitHub (release 0.4.0) – SubNavigation.tsx

Notice how stopPropagation() is not included in the top switch. This is because the button component I use from react-aria-components already calls stopPropagation() itself, so I don’t need to set it. This may differ from the button component you are working with. Otherwise, the code for SubNavigation is similar to that already seen in NavigationItem.

Return to Content Links

Summary

Handling link references to multiple components becomes straightforward when a provider holds the data and the functions that call and manipulate it. A hook passes through necessary provider functions as well as its own to provide keyboard handling within a single list.

With all the code added to support single-list keyboard handling, the question arises: Does it work?

Part of the issue with demonstrating keyboard handling is the inability to show which key is being used as focus moves. It’s one of the reasons keyboard accessibility is so hard to demonstrate during sprint demos. I’ll be providing videos I make that detail what I am doing and which keys I’m pressing as I guide you through the enhancements.

I’ll be back with another article soon, focusing on CSS design and applying a layout to the default horizontal navigation.

9 SaaS development companies worth knowing (a technical look)

Most agency roundups are written for product managers and founders. This one isn’t.

If you’re a CTO, lead engineer, or the person who’ll actually own the codebase after the agency hands it off, you care about different things: what stack they default to, whether they write tests, how the CI/CD is set up on day one, and who’s actually on the team. Not logo counts.

Here’s the same list, filtered for what engineers want to know.

1. Brocoders

Stack: React, Node.js, Python/Django, PostgreSQL, AWS/GCP

The default setup is a React frontend against a Node.js API layer, Postgres for the data store, and AWS for infra. They use GitHub Copilot and similar tools in their engineering workflow, which shows up in hour estimates, and they publish production-ready SaaS boilerplates at bcboilerplates.com covering auth, user management, billing, and admin patterns.

For multi-tenant SaaS, Brocoders go schema-per-tenant on Postgres (row-level security where performance allows) with per-tenant rate limiting at the API layer. GraphQL is available as an API layer option, though REST is the default.

CI/CD is set up as a line item in discovery, not an afterthought. Typical pipeline: GitHub Actions, containerized builds, staging and prod environments separated from day one.

Team composition: cross-functional from the start, product strategist included in early sprints. Engineers stay on the account; there’s no discovery team that hands off to a separate build team.

Clutch: 5.0 across 38 reviews, which is statistically unusual. Read the actual reviews, not just the score.

2. Netguru

Stack: React, React Native, Ruby on Rails, Node.js, AWS

Primarily a Rails and React shop with a strong design practice. Good choice if your MVP needs polished UX before your Series A. Less obvious choice if your architecture needs to be GraphQL-native or if you’re building something data-heavy.

Their engineering process is Jira + Figma + Slack, two-week sprints, and a dedicated PM. Code quality reviews come up consistently in client feedback.

Best technical fit: design-led consumer SaaS, early-stage fintech, mobile-first products.

3. Upsilon

Stack: Cloud-native SaaS, AWS-first

Lean team (10-49 engineers), US-headquartered. Built 25+ SaaS MVPs. Their model is speed to functional prototype, so they’re well-matched for founders who need working software in under 3 months to validate with real users.

Less established on enterprise compliance. If you need SOC 2 controls baked in from sprint one, ask explicitly.

4. Intellectsoft

Stack: React, Node.js, .NET, AWS, blockchain where relevant

The compliance-first option. Their work is in regulated industries: healthcare, fintech, legal. They run strong test coverage and have documented QA processes. On-time record is solid. Not for sub-$50K engagements.

If you’re building something that needs HIPAA controls or a financial audit trail, they’re worth a serious look. Ask about how they handle secrets management and access control audits in the discovery phase.

5. ScienceSoft

Stack: .NET, Java, React, Azure/AWS, Salesforce, ServiceNow

30+ years old, which means they’ve seen things go wrong at scale. The $5K minimum is genuinely rare for the quality tier they operate at. Their breadth (cybersecurity, SharePoint, mobile, healthcare IT) is both a strength and a risk: you want to know which vertical your project lands in and who specifically is assigned.

Incident response under 24 hours is documented across multiple reviews. That’s the number that matters at 2am.

6. Railsware

Stack: Ruby on Rails, React

The expensive option ($100-$149/hr) and proud of it. Portfolio includes GitLab, Calendly, and Buffer. Their engineers challenge assumptions in discovery, which you want from a partner who’ll be responsible for architecture decisions.

If you’re building Rails-native B2B SaaS and want minimal technical debt, they’re genuinely competitive at that. If you’re expecting microservices or a Go/Rust backend, they’re probably not the fit.

7. Simform

Stack: AWS, DevOps-first, IaC heavy

Cloud-native engineering with strong DevOps automation. Good if your architecture needs serious IaC (Terraform, CDK) from the start and you want blue/green deployments as a standard, not a nice-to-have.

The AI engineering story is less clear. Before scoping, ask for concrete examples of MLOps pipelines, not just “we support AI features.”

8. Intellias

Stack: AWS/GCP, microservices, data platforms, AI/ML integrations

1,000-9,999 engineers, enterprise contracts, multiple scrum teams in parallel. If you’re migrating a monolith to microservices or need 5 teams running concurrently, they have the depth to do that. If you’re a 3-person startup, you’ll likely get junior staffing.

Notable on production reliability: near-zero incident rate comes up in reviews. Jira-based project management, Clutch 4.9.

9. ELEKS

Stack: ML/BI integration, data engineering, standard web SaaS

2,100+ engineers, founded 1991. If your SaaS is data-heavy and you need ML features (churn prediction, demand forecasting, anomaly detection) treated as first-class concerns with proper MLOps, ELEKS has the data engineering depth for that. Ask for case studies with specific metric lifts, not just “we used ML.”

What to check before you sign

A few things that matter more than stack preferences:

CI/CD on day one. Ask if pipeline setup is a line item in the discovery phase or something bolted on later. Agencies that treat it as optional tend to treat monitoring as optional too.

Who’s actually on the account. Get names and LinkedIn profiles of the engineers who’ll work on your project. Ask if they stay for the duration or rotate off. The engineer who did discovery should be in the first sprint.

Hour estimates and tooling disclosure. Ask directly: “Do your engineers use AI coding assistants? Which ones? How does that affect your estimates?” A vendor using GitHub Copilot or Cursor should be estimating 30-50% fewer hours for standard modules. If they’re not, you’re paying for reinvented wheels.

Multi-tenant architecture decision. If you’re building SaaS with multiple customers on shared infrastructure, ask how they handle tenant isolation. Schema-per-tenant vs. row-level security have very different performance profiles at scale. Get an opinion in discovery, not post-launch.

Post-launch on-call. Ask who’s on call after go-live and what the SLA is for critical bugs. “We’ll respond quickly” is not an SLA.

Material Nova — The Best VS Code Theme of 2026


🎨 Material Nova — The Best VS Code Theme of 2026

“Your editor is where you spend 8+ hours a day. It deserves to be beautiful.”

If you’ve been jumping between One Dark Pro, Dracula, Tokyo Night, and Ayu trying to find the one — your search is over.

Material Nova is here. And it’s not just another color scheme — it’s a premium, meticulously engineered coding workspace built on Google’s legendary Material Design palette.

🤔 Why Another VS Code Theme?

Fair question. The marketplace has hundreds of themes. So what makes Material Nova different?

Most themes are ports — someone grabs a popular palette, slaps it into a JSON file, and ships it. Material Nova was built differently:

  • ✅ Deeply optimized semantic highlighting for JS, TS, React (TSX/JSX), Python, C#, HTML/CSS, and JSON
  • ✅ WCAG-compliant contrast ratios to reduce eye strain during late-night sessions
  • ✅ 15 distinct variants for every mood, lighting condition, and aesthetic preference
  • ✅ High Contrast editions for every dark variant
  • ✅ Zero performance impact — pure hand-crafted JSON, no background scripts

Material Nova v2.1.0 just dropped with two brand-new variants — Nebula and Sakura. Read on for the full breakdown.

🎭 15 Variants — One for Every Mood

This is where Material Nova truly shines. Whether you’re coding at 2am or pair-programming in a sunny office, there’s a variant made for you.

🌑 Dark Variants

Variant Background Vibe
Material Nova Default #263238 The classic Google Material experience
Material Nova Darker #212121 Deep matte gray for ultimate night coding
Material Nova Palenight #292D3E Vibrant purple-blue synthwave aesthetic
Material Nova Ocean #0F111A Calming deep-sea navy layout
Material Nova Deepforest #141F1D Organic dark forest green comfort
Material Nova Sakura 🌸 #1D1921 Premium cherry blossom-inspired rose charcoal
Material Nova Nebula 🌌 #12101E Celestial deep space galactic aesthetic

💡 Light Variant

Variant Background Vibe
Material Nova Lighter #FAFAFA Perfect for bright daylight or outdoor offices

⚡ High Contrast Editions

Every dark variant ships with a High Contrast (HC) sister edition for developers who prefer sharp borders and maximum visibility — including a true OLED pitch-black option with #121212 and #09080F backgrounds.

🌌 What’s New in v2.1.0

The latest release is the biggest update yet:

Introducing Material Nova Nebula

A stunning galactic-inspired dark theme with deep space aesthetics. Background #12101E, foreground #E1DFEE — feels like coding among the stars. The HC variant pushes to OLED black at #09080F.

Introducing Material Nova Sakura 🌸

The most-requested theme from the community. A soft rose-charcoal layout inspired by cherry blossoms — background #1D1921, foreground #E3D6DF. Elegant, warm, and unlike anything else on the marketplace.

Automated Theme Compilation

Palettes are now compiled using standard-version automation — ensuring consistency across all 15 variants with zero runtime overhead.

⚡ Install in 60 Seconds

# Option 1: From VS Code Command Palette
1. Open Extensions panel → Ctrl+Shift+X (or Cmd+Shift+X)
2. Search: Material Nova
3. Click Install
4. Press Ctrl+K → Ctrl+T to open theme picker
5. Select your variant. Done. ✅

# Option 2: Via CLI
code --install-extension dhavalkurkutiya.vsc-material-nova

⚙️ Recommended Settings

Get the absolute best experience with these settings.json tweaks:

{
  "editor.fontFamily": "JetBrains Mono, Fira Code, monospace",
  "editor.fontSize": 14,
  "editor.lineHeight": 1.6,
  "editor.letterSpacing": 0.5,
  "editor.fontLigatures": true,
  "editor.cursorBlinking": "smooth",
  "editor.cursorSmoothCaretAnimation": "on"
}

Font tip: Pair Material Nova with JetBrains Mono for the sharpest ligature rendering. It’s free and the difference is night and day.

🆚 How It Compares

Feature Material Nova One Dark Pro Dracula Tokyo Night
Variants 15 4 3 6
High Contrast editions ✅ All variants ❌ ❌ Partial
Semantic highlighting ✅ Deep Partial Partial Partial
WCAG contrast compliance ✅ ❌ ❌ ❌
OLED black support ✅ ❌ ❌ ❌
Background scripts Zero None None None

🏁 Final Verdict

If you care about your editor environment — and you should — Material Nova is the most complete VS Code theme available right now.

15 variants. Semantic highlighting. WCAG-compliant contrast. Zero performance cost. And it’s completely free.

Stop settling for a theme that’s “good enough.” Your 8-hour-a-day workspace deserves better.

→ Install Material Nova on VS Code Marketplace

👨‍💻 About the Author

Material Nova is crafted with passion by Dhaval Kurkutiya.

Licensed under MIT — free to use, customize, and enjoy. Made with ❤️ by Dhaval Kurkutiya.

Inference Routing Is Becoming an Infrastructure Placement Problem

inference placement orchestration — the Inference Execution Plane governing substrate selection across GPU cluster, dedicated silicon, and sovereign on-prem

The request arrives. The model answers. For most teams, everything in between is invisible — a gateway rule, a load balancer entry, maybe a classifier someone wrote three months ago. That worked when inference meant one cluster and one model family. The execution environment was fixed, so the routing decision was trivial.

That assumption is gone. Enterprise inference now spans GPU clusters, dedicated inference silicon, giant-context processors, provider APIs, and sovereign on-premises substrates — each with different physics, different cost models, and different failure domains. Every routing decision is now implicitly a placement decision. And the application layer was never designed to make it correctly.

Inference placement orchestration is the discipline of governing those decisions at the infrastructure level — where the signals, the authority, and the system visibility actually exist.

inference placement orchestration — the Inference Execution Plane governing substrate selection across GPU cluster, dedicated silicon, and sovereign on-prem

Note: Cost-aware model routing asks: which model should answer this request? Infrastructure-aware inference placement asks: where should this request execute — on which substrate, under which topology, within which latency and sovereignty constraints? Those are no longer the same problem. The first is covered in Cost-Aware Model Routing in Production. This post covers the second.

The Routing Frame Is Obsolete

The API gateway inherited inference routing the same way it inherited everything else — by default, because it was already in the path. When inference ran on a single cluster against a single model family, that was fine. The gateway forwarded the request. The cluster handled it. Routing was endpoint selection, and endpoint selection was trivial because there was essentially one endpoint.

That architecture encoded an assumption that no longer holds: that the execution environment is fixed. Once inference spans multiple substrate types, the routing decision stops being about which model handles the request and starts being about which execution environment the request enters. Those are materially different problems.

An API gateway that was designed to balance HTTP traffic across homogeneous nodes is not equipped to arbitrate between a GPU cluster optimized for throughput, a dedicated inference accelerator optimized for deterministic low latency, and a provider API optimized for burst elasticity. The decision variables — physics, cost tier, latency class, failure domain, sovereignty boundary — exist at the infrastructure layer. The gateway doesn’t see them.

The teams that recognize this shift early won’t be scrambling to retrofit placement logic into application code when they add their second substrate type. The ones that don’t will discover the problem the hard way: through latency variance they can’t explain, cost anomalies that don’t map to request volume, and sovereignty violations that the application registered as successful responses.

Inference Placement Orchestration Requires a New Architectural Layer

What’s needed is a formal definition of what has been informally accumulating as scattered decisions across application code, gateway configs, and ad-hoc infrastructure policy.

The Inference Execution Plane is the infrastructure layer responsible for substrate selection, topology awareness, latency SLA enforcement, and execution cost assignment across heterogeneous inference substrates. It is not a product category. It is an architectural function — the placement authority layer that governs where inference workloads execute and under what constraints.

This distinction matters: the Inference Execution Plane is not the inference platform. It is not Kubernetes for AI, inference middleware, or orchestration tooling. Those systems manage the lifecycle of inference services. The Execution Plane governs the placement decisions that determine which service handles which workload, under which infrastructure conditions, within which policy boundaries.

The function exists in every multi-substrate inference deployment today. The problem is that it’s distributed across systems that weren’t designed to own it — application routers with no infrastructure telemetry, gateway configs that hardcode substrate assumptions, and team boundaries that give application engineers authority over decisions that require infrastructure visibility.

Formalizing the Inference Execution Plane as an explicit architectural layer is the first step toward placing that authority correctly.

inference execution plane — placement authority layer governing substrate selection, topology awareness, and latency SLA enforcement

Multi-Substrate Inference Is a Physics Problem Disguised as an API Problem

The most common misread of the inference routing problem is that it’s a software problem — better classifiers, smarter gateways, more granular routing rules. That framing misses the underlying issue.

Multi-substrate inference is fundamentally a physics problem. Each substrate class carries materially different execution characteristics that determine whether a given request belongs there:

Substrate Optimized For Governing Constraint
GPU cluster Throughput, parallel batch Memory bandwidth, tensor parallelism
Dedicated inference silicon (Groq-class) Deterministic low latency Fixed execution timing, queue depth
Giant-context processors (Cerebras-class) Large context windows On-chip SRAM locality
Provider API Burst elasticity External rate limits, egress cost
Sovereign on-premises Compliance boundary Jurisdiction, air-gap policy

These aren’t performance tiers. They’re architecturally distinct execution environments with different physics. Routing a latency-sensitive synchronous request to a throughput-optimized GPU cluster doesn’t just affect response time — it enters a different queuing model, a different memory locality regime, and potentially a different failure domain. The request may succeed. The infrastructure cost of that success may be invisible to the application.

This is what separates inference placement from the training/inference hardware split that became visible at GTC 2026. Hardware fragmentation didn’t create the placement problem — it made it unavoidable. When a single GPU cluster handled everything, substrate arbitration didn’t matter. Once training and inference silicon diverge, and dedicated inference accelerators enter the stack, every request carries an implicit substrate assignment. The question is whether that assignment is made deliberately or by default.

Deterministic networking becomes relevant here at a layer most teams haven’t reached yet: when the interconnect itself is part of the inference execution path, fabric topology becomes a placement input. The request isn’t just selecting a substrate — it’s selecting a path through an interconnect fabric with its own latency characteristics, congestion domains, and saturation thresholds.

This is workload arbitration. Calling it routing undersells the problem.

Application Routers Are Blind to Infrastructure Signals

The practical consequence of application-layer routing is a structural information gap. Application routers are optimized for what they can observe. Infrastructure signals are outside their observability boundary entirely.

The two signal sets don’t overlap:

Application-visible: request type, token count, tenant identity, SLA tier, model preference

Infrastructure-visible: rack congestion, GPU memory pressure, interconnect saturation, inference queue depth, east-west traffic load, regional failover state, reserved vs. burst substrate allocation, power and cooling headroom

The application router optimizes locally against the signals it can see. The infrastructure absorbs the cost of decisions made without the signals it holds. On a single substrate, this gap is manageable. On multi-substrate deployments, it becomes a structural failure mode.

Locality Collapse is the formal name for what happens when this gap goes unaddressed. It is the loss of topology-aware execution locality caused by routing systems that optimize for endpoint availability rather than infrastructure placement efficiency. The consequences compound as the substrate footprint grows: hidden cross-zone bandwidth amplification, interconnect congestion from misrouted workloads, latency variance that doesn’t track request complexity, sovereignty leakage from spillover that the application never flags, and egress multiplication from cross-region routing that was never modeled.

The most acute expression of Locality Collapse is inference spillover: the primary substrate saturates, the router spills traffic to the next available endpoint — typically a provider API — and the application layer registers a successful response. From the application’s perspective, the request resolved. From the infrastructure’s perspective, a sovereignty boundary may have been crossed, egress cost multiplied, latency doubled, and the spillover event went completely unlogged because the routing system had no model of substrate boundaries.

locality collapse and inference spillover — routing without substrate topology awareness causing sovereignty leakage and egress amplification

Placement Authority Migrates to the Infrastructure Control Plane

The core problem is not that application teams lack visibility into infrastructure signals. It’s that application code has no mechanism to arbitrate shared infrastructure scarcity — and multi-substrate inference creates exactly that condition.

When inference runs on shared GPU infrastructure, placement decisions affect every tenant on the cluster. An application router making substrate assignments without infrastructure telemetry cannot know whether it is consuming reserved capacity, triggering burst allocation, contributing to interconnect saturation, or pushing a congested substrate further into degradation. These are infrastructure governance problems. Solving them requires infrastructure authority.

What application teams cannot own:

  • Cross-region execution decisions
  • GPU tier consumption across tenants
  • Sovereignty boundary traversal
  • Congestion avoidance across shared fabric
  • Interconnect utilization policy
  • Reserved vs. burst substrate allocation
  • Power-aware placement under capacity constraints

The correct owner is the infrastructure control plane. The implementation patterns vary — topology-aware placement engines, inference schedulers with live infrastructure telemetry feeds, policy-aware substrate brokers — but the architectural principle is consistent: placement decisions require the global infrastructure visibility that only the platform layer holds.

This is the same migration that has happened at every prior layer of infrastructure complexity. The CI/CD pipeline became the infrastructure control plane for configuration and deployment. The CLI became a governance surface when agentic systems started consuming it. Inference placement is following the same arc — authority is migrating to the layer that has the system visibility to exercise it responsibly.

inference placement authority migrating from application layer to infrastructure control plane

Inference Placement Is the New Load Scheduling

The transition inference placement is undergoing has a direct precedent in infrastructure history.

VMs needed schedulers. When virtual machines proliferated, placement decisions moved out of manual admin processes and into infrastructure-layer schedulers. DRS, SDRS, affinity rules. The application team didn’t own host placement. The platform layer did.

Containers needed orchestrators. When containers scaled beyond what manual placement could manage, Kubernetes took over bin packing, resource negotiation, and topology-aware scheduling. Application teams don’t own node placement. The orchestrator does.

Inference workloads now need topology-aware execution scheduling. The pattern is identical. The execution environments have fragmented. The placement variables — substrate physics, interconnect topology, sovereignty policy, cost tier — exceed what application-layer routing can manage with the signals available to it. The function needs to move.

What’s different this time is the compression. The transition from VMs to schedulers took years. The transition from containers to orchestrators took years. The hardware substrate split in inference happened over months, driven by dedicated silicon entering production alongside GPU clusters at a pace that enterprise planning cycles weren’t designed to absorb.

The Invisible-Until-Scale Failure Mode

Single-substrate inference deployments hide placement problems. The topology is simple, the substrate is fixed, and the routing decision space is small. Multi-substrate deployments expose placement problems as distributed systems problems. The failure mode isn’t a discrete event. It’s variance accumulation.

The first symptoms are almost never outages. They’re measurement anomalies: latency jitter that doesn’t track request complexity, inconsistent token throughput across nominally identical requests, degraded tail latency that looks like infrastructure noise, regional quality divergence that only appears in percentile metrics, inference queue oscillation that smooths out before anyone investigates. By the time the failure mode is clearly legible as a placement problem, it has typically been accumulating for weeks.

This is the same pattern documented in autonomous systems drift — gradual degradation that produces no single alerting event until the accumulated variance crosses a threshold. Inference observability at the execution layer, not just the model output layer, is the prerequisite for catching this early. Without placement-level telemetry, the variance looks like noise. With it, the pattern is identifiable.

The specific failure sequence: a degraded inference node stays in the routing pool because the application router has no infrastructure health signal — only endpoint availability. Traffic continues flowing to a substrate that is congested, throttled, or queued deep. The application sees elevated latency and assumes model load. The infrastructure knows the substrate is saturated. The routing system has no mechanism to act on that knowledge because the signal doesn’t cross the application/infrastructure boundary.

Architect’s Verdict

Inference routing is following the same arc as every other placement problem in enterprise infrastructure. It starts in the application layer because that’s where the first workload lives and the first engineer with a deadline made a decision. It stays there until the complexity of shared infrastructure, heterogeneous substrates, and cross-boundary policy makes application-layer authority untenable.

The organizations that scale AI infrastructure successfully will not be the ones with the best models. They will be the ones that turn inference placement into an infrastructure discipline before scale forces it on them — before the second substrate type enters production, before the first sovereignty incident, before the first multi-tenant GPU contention event exposes the placement logic that was never designed to handle it.

The ones that don’t will eventually discover they built a distributed infrastructure scheduler inside application code — without the telemetry, the placement policy, or the authority to operate it correctly.

Additional Resources

Originally published at rack2cloud.com

I just build a League MBTI Analytics

 Does your real-life personality match your playstyle? I built a tool that translates your 2026 playstyle data into an MBTI Archetype with dynamic share cards!

Instead of a generic text quiz where people lie to make themselves look better, this analyzes how you actually act under pressure. I want to see if the mapping holds up at scale. Did it accurately call out your playstyle flaws or your main champion pool? Roast the UI, critique the data logic, or flame my archetype mapping in the comments—I’ll be hanging out to answer questions and tweak the algorithm live based on your feedback.

İddia: “PlayStation State of Play etkinliğinde Hogwarts Legacy 2 ve yeni God of War oyunu tanıtılabilir”

İddia:

İddia: "PlayStation State of Play etkinliğinde Hogwarts Legacy 2 ve yeni God of War oyunu tanıtılabilir"Sony dün 2 Haziran tarihli yeni “State of Play” etkinliğini duyurdu. 60 dakika süreceği açıklanan etkinlikte “Marvel’s Wolverine” başta olmak üzere PS5’e gelecek çok sayıda oyun hakkında yeni bilgiler verilecek ve görüntüler paylaşılacak. İddiaya göre etkinlikte uzun süredir merakla beklenen Hogwarts Legacy 2 de duyurulabilir. Resmi olarak 2027’de çıkacağı düşünülen oyuna ek olarak, Faye’ye odaklanan yeni bir “God of War” duyurusu da “State of Play” beklentileri arasında yer alıyor.

İddia: “PlayStation State of Play etkinliğinde Hogwarts Legacy 2 ve yeni God of War oyunu tanıtılabilir” yazısı ilk olarak Teknoloji haberleri – LOG üzerinde yayınlandı.

Why I Built My Own Site with Astro, Not WordPress when I use WordPress for a Living

The short version — WordPress is a brilliant tool — it’s also the wrong tool for a site that’s nothing but articles, calculators, and a sitemap. Tinkernotes has no logins, no comments, no checkout, and no database it can’t live without. So it’s an Astro site built as static HTML and served from Cloudflare Pages. The result is a site that costs £0/month to host, ships almost no JavaScript, and can’t be hacked the ways WordPress sites get hacked. If you’re a WP dev sitting on a content project, this is the build I’d push you towards.

I’ve spent eight years shipping WordPress and WooCommerce for a UK agency — custom plugins, themes, gateway integrations, the lot. WordPress pays my mortgage. So the most reasonable question about this site is: why isn’t it WordPress? I’ve had that question from two developers I respect already. Here’s the long answer.

The choice I was expected to make

If you’d asked me to build a content site any year from 2014 to about 2022, the answer was reflex: WordPress. Pick a host, install WP, drop in a block theme or build one, add Yoast for SEO, write. I could have had Tinkernotes live in an afternoon and never thought about it again. That’s not a knock on WordPress — that is WordPress’s superpower. It collapses “publish a website” into a solved problem.

But “I already know how” is a sunk cost, not a reason. So before defaulting, I wrote down what this site actually has to do:

  1. Render articles, reviews, and guides from text I write.
  2. Run two interactive calculators in the browser.
  3. Generate a sitemap, meta tags, and structured data.
  4. Be fast, and stay cheap.

That’s the whole list. Notice what’s not on it: no user accounts, no comments, no shopping cart, no form that writes to a database, no content edited by anyone but me. The moment I saw the list written out, WordPress stopped looking like the obvious answer and started looking like a lot of moving parts solving problems I don’t have.

What WordPress would be carrying that I don’t need

A standard WordPress install is a PHP application that boots on every request, talks to a MySQL database, runs the plugin and theme hook stack, and assembles HTML — then you bolt a caching plugin on top to avoid doing most of that. It’s a dynamic system pretending to be a static one because, for a content site, static is what you actually want.

Everything dynamic about WordPress is also its attack surface and its maintenance bill:

  • The database. A dependency, a backup job, and a thing that can get out of sync.
  • PHP version drift. Hosts deprecate, plugins lag, you babysit the gap.
  • Plugin updates. Yoast, a cache plugin, a security plugin — each one a small recurring risk and another notification.
  • The login page. /wp-admin and /wp-login.php are probed by bots within minutes of going live. Every WP site I’ve ever run shows it in the logs.

None of that is hard for me — I do it for clients every week. But it’s all labour, and it’s labour in service of capabilities this site will never use. Zero of those moving parts make a single article load faster for a reader.

The framing

A blog is read far more often than it’s written. WordPress optimises the writing moment — the dashboard — and pays for it on every read with a PHP and database round-trip. A static site inverts that: do the work once at build time, then serve plain files forever.

The stack I chose, and what each piece does

Nothing here is exotic. It’s the boring, well-trodden static stack — which is exactly the point.

Astro is the framework. I write content as MDX — Markdown with frontmatter, plus the ability to drop components into the prose. Astro renders every page to plain HTML at build time and ships zero JavaScript by default. The interactive bits — the 3-year cost calculator and the hosting cost calculator — are Svelte “islands”: JS loads only for that component, only on the pages that use it. The article you’re reading ships no framework JavaScript at all.

Cloudflare Pages hosts it. I push to main on GitHub, Cloudflare builds the site and serves the static output from its edge network. There’s no origin server to keep patched, because there’s no origin server. The free tier covers unlimited bandwidth and 500 builds a month — comfortably more than a one-person site needs.

Git is the CMS. Content lives in the repo as .mdx files. Every edit is a commit; every commit is a diff, a history, and a one-line rollback. I get drafts via branches and review via pull requests — the workflow I already use for code, now also for prose.

For a developer this is a feature, not a compromise. For a non-technical client it would be a non-starter, and that distinction matters — I’ll come back to it.

What I gave up coming from WordPress

I want to be honest about the trade, because “just use a static site” advice usually isn’t.

What I gained

  • No server to patch, no PHP version to chase, no database to back up
  • Hosting cost is £0/month on Cloudflare Pages — and stays £0 under traffic spikes
  • No /wp-admin to be brute-forced; the attack surface is basically static files
  • Pages render as pre-built HTML — fast without a caching plugin bolted on
  • Content is versioned in Git: real diffs, real history, instant rollback

What it cost me

  • No dashboard — publishing means a commit and a build, not a “Publish” button
  • No mobile editing, no “fix a typo from my phone in the pub” moment
  • A content rebuild runs on every change; fine at this size, slower at thousands of pages
  • The plugin ecosystem is gone — anything WP gives you free, you wire up yourself
  • It is a developer workflow: hand this stack to a non-dev client and it falls over

The last one is the real boundary. WordPress’s enduring genius is that it hands a non-technical person a usable website and the means to run it. The day-to-day editor isn’t meant to touch code. Astro-on-Git assumes the editor is the developer. For Tinkernotes — a personal site I both build and write — that assumption holds perfectly. For most of my agency clients it would be indefensible.

What changed once it was live

Three things, concretely.

Performance came for free. No caching plugin, no CDN to configure, no image optimisation stack to tune. The pages are already static HTML with hashed assets, served from the edge. I set a Lighthouse budget of 95+ and hitting it has mostly been about not breaking it, rather than chasing it.

The security worry just… left. There’s no admin login to harden, no plugin CVE feed to watch, no “is this theme still maintained” question. A static file can’t run a SQL injection. That’s not nothing — a meaningful share of agency emergency work is cleaning up compromised WordPress installs.

Cost went to zero and stayed there. No managed WordPress host, no renewal-rate surprise twelve months in. If a post ever does numbers, Cloudflare’s edge absorbs it on the same free tier — a static file under load is still just a static file. (Renewal-rate games are exactly the trap I dig into in the Kinsta UK review and Hostinger UK review — worth a read if you are hosting WordPress.)

When I’d still reach for WordPress — and I would

This isn’t a conversion story. WordPress is still the right call when the site needs the things WordPress is for:

  • A non-technical person has to publish and manage content independently.
  • The site is genuinely dynamic — accounts, comments, gated content, member areas.
  • It’s a shop. WooCommerce on WordPress is still where I’d start most UK ecommerce builds — the whole Shopify vs WooCommerce comparison exists because that decision is rarely obvious.
  • You need to move fast with a known team and a deep plugin ecosystem behind you.

The mistake isn’t choosing WordPress. The mistake is choosing it by reflex — defaulting to a dynamic CMS for a job that’s pure content, then spending the project budget caching and securing your way back to the static site you could have built directly.

The takeaway

Match the tool to the workload, not to your CV. Tinkernotes is content and calculators with one developer-author, so it’s static HTML on the edge. Your next build might point straight back at WordPress — and that’s fine, as long as you actually looked.

If you want the longer version of who’s writing this and why, the about page covers it. The short version: I still write WordPress for a living. I just stopped assuming it was the answer before I’d heard the question.

More from Tinkernotes: