A visual essay

Extensible Software in the Age of LLMs

Why ordinary web software strands the long tail of individual needs, why LLMs change the economics of extension, and what it takes to run untrusted code safely.

Start scrolling
1

The long tail of unmet needs

Most web software is static. Developers serve the head of the demand curve and leave a long tail of individual needs unmet — because every added feature makes the product worse for everyone who doesn’t need it.

The head of the curve gets the roadmap. The tail gets workarounds, spreadsheets, or nothing. As more features are crammed in, the interface accretes buttons, toggles, and modes that most users never wanted. The product becomes worse for the majority in order to satisfy a minority.

2

Software has gotten “squishy”

LLMs excel at building Software for One — personal tools custom-fit to a single workflow. The result is software with a battle-tested core that is endlessly extensible just by asking.

“Agents make it easy to build personal tools for yourself or your team. But deploying, securing, and sharing that software is still far more complicated than creating it. A cloud built for small software could remove that complexity and make bespoke tools as easy to share with a colleague as a Google Doc.”
— Pete Koomen, Y Combinator on “Small Software”

In the past year, users suddenly gained the ability to speak code into existence. Most existing software can’t leverage that. Pi does. But most pluggable software today is local professional tooling — AI agents, IDEs, game mods, Blender add-ons — with a high barrier to entry.

The web is the most successful software distribution system in the world. It shouldn’t be left behind.

3

The hypothesis: a new opportunity for extensible software on the web

LLMs lower the cost of authoring extensions. Modern sandbox primitives lower the cost of deployment and provide security boundaries. Build a solid, accountable core, and let users safely extend it by having an LLM fill in the missing pieces.

A robot extrudes the silly bits of code, hooks them into extension points, and the user can share the result with others. Instead of asking a user to build a separate service, the logic lives inside the app, governed by the same sandbox.

4

Where this matters

The extension pattern applies wherever a core product meets a diverse set of user workflows. Four places where it is especially pressing.

AI agents

Rather than bloating the harness, agents can provide stable hooks for tools, commands, events, and UI. A request becomes a small TypeScript extension, reloads in place, and can be shared.

“…nearly everything is an internal plugin — 68 of them — which means any behavior can be disabled and the plugin APIs are properly dogfooded.”
— OpenCode, on building with internal plugins
5

Why it’s hard

Obsidian proves the power of extensibility — and its cost. You must trust every plugin. That works for low-stakes notes, but falls apart for customer records, financial transactions, or private messages.

Executing arbitrary user code raises a wall of threats. The dream of effortless customization meets the reality that one malicious or careless snippet can harm every other tenant. The wall is not a reason to give up; it is the specification for the primitive we need.

6

The existence proof: Salesforce

Since 2007 — before S3 and EC2 were even a year old — Salesforce has run customer code safely at immense scale as a multi-tenant programmable platform.

@RestResource(urlMapping='/customer-health')
global with sharing class CustomerHealthApi {
    @HttpGet
    global static Account getCustomer() {
        // Salesforce handles routing, auth, tenant isolation, execution.
        // No web server to deploy.
        Account a = [SELECT Id, Name, Health_Score__c
                     FROM Account
                     WHERE OwnerId = :UserInfo.getUserId()
                     LIMIT 1];
        return a;
    }
}
public class RenewalScanner implements Schedulable {
    public void execute(SchedulableContext context) {
        // Flag accounts with renewals in the next 30 days
        List<Account> accounts = [
            SELECT Id, Needs_Attention__c
            FROM Account
            WHERE Renewal_Date__c <= NEXT_N_DAYS:30
        ];
        for (Account a : accounts) {
            a.Needs_Attention__c = true;
        }
        update accounts;
    }
}
// Schedule to run daily at 2 a.m.
System.schedule('RenewalScanner', '0 0 2 * * ?', new RenewalScanner());
// A modern Workers-style exported handler
export default {
  async fetch(req, env, ctx) {
    const { account } = await env.DB.prepare(
      `SELECT id, name, health_score
       FROM accounts WHERE owner_id = ?`
    ).bind(env.user.id).first();
    return Response.json(account);
  },
} satisfies ExportedHandler<Env>;
{
  "crons": ["0 2 * * *"]
}
export default {
  async scheduled(controller, env, ctx) {
    await flagRenewalsInNext30Days(env.DB);
  },
} satisfies ExportedHandler<Env>;

Salesforce had to build a compiler, type system, runtime, debugger, and an educational ecosystem. The business value justified it. In 2026, we have far more options — but the core requirement remains: safely run custom logic in response to app events, within transactions.

7

The new primitive and its five required properties

This is the heart of the argument. An extension primitive must satisfy five properties. Each one is a constraint that turns the threat wall into an engineering target.

1

Cheap to run

~$0 when idle, tiny fractions of a penny per execution. Containers-per-user are a non-starter.

2

Fast cold starts

Single-digit milliseconds when user code is on the request’s critical path.

3

Control over limits

CPU, memory, network, response size, log volume and rate.

4

Solid isolation boundary

Fault and security isolation: crashes, infinite loops, memory bombs, Spectre.

5

Safe action through capabilities

Hand untrusted code narrow references to approved functions. Remove ambient I/O.

Cheap to run

With thousands or millions of users running snippets, per-user containers are economically impossible. Idle cost must be near zero and per-execution cost microscopic. Memory overhead per tenant directly determines machine density.

The Heroku “hello world” log flood

A popular getting-started guide once told users to deploy:

while True:
    print("hello world!")

A brand-new app instantly started spewing millions of log lines per second, forever. Without limits on CPU, log rate, and memory, one tutorial can devastate a platform.

Capability lab: try to exfiltrate the credential

The subtlest property is safe action. The same task — “fetch one approved email” — can be exposed four different ways. Try each model and see if you can leak the credential.

Select a model and run an action.
export default function shouldWeOrderPizzaTonight(data: Input): boolean {
  const haveFoodAtHome = data.fridge.hasIngredients;
  const haveEnergy = data.body.checkCapacity;
  const haveTime = !data.schedule.isTight;
  return true;
}
8

The technology options

Four broad approaches, presented as an honest comparison. The same five properties matter for agent execution platforms too: running logic on behalf of a user you cannot trust is the same problem.

Interpreter

Lua, QuickJS, or roll-your-own. The Salesforce path: full control, full responsibility.

V8 isolates

Google-hardened JavaScript engine. Cloudflare Workers, isolated-vm, Rivet secure-exec, celld.

MicroVMs

Firecracker, libkrun, AWS Lambda MicroVMs, @deno/sandbox, smolvm, Tensorlake, Daytona. Near-VMs under a second.

WASM + WASI

Blank slate with no ambient HTTP or env. Host defines capabilities. Composes with isolates or microVMs.

Where the primitive lacks a capability model, consider an object-capability protocol like Cap’n Web. The options also compose: WebAssembly can run inside a V8 isolate, and microVMs are useful for authoring and testing even when the runtime is an isolate.

9

Cloudflare Dynamic Workers: the highlighted answer

Disclosure: Jeremy Morrell works at Cloudflare. This section reflects the essay’s argument that Dynamic Workers are the closest thing in 2026 to a production-ready framework for extensible web apps.

export async function analyzeArticle(env: Env, article: Article) {
  const result = await env.AI.run(
    '@cf/meta/llama-3-8b-instruct',
    {
      messages: [
        {
          role: 'system',
          content: 'Decide whether the supplied article talks about cute kittens.',
        },
        { role: 'user', content: article.text },
      ],
    }
  );
  return result;
}
import { transform } from 'sucrase';

export function transpileUserCode(source: string): TranspileResult {
  try {
    const result = transform(source, {
      transforms: ['typescript'],
      disableESTransforms: true,
    });
    return { type: 'success', code: result.code };
  } catch (err) {
    return { type: 'failure', error: String(err) };
  }
}

Dynamic Workers are not the only answer, but they bundle the primitive, the storage, the execution, the observability, and the LLM in one place. That is the difference between a theoretical sandbox and a platform users can actually ship.

10

Platforms are hard — but worth it

Platforms are hard to design, hard to run, and hard to debug. But they are worth it, because you can be genuinely surprised by what your users build.

The long tail of needs is not a niche edge case. It is the natural shape of software demand. LLMs make it cheap to address. Sandboxes make it safe. The only question left is what kind of platform you will build on top of the primitive.