{"slug": "extensible-software-in-the-age-of-llms", "title": "Extensible Software in the Age of LLMs", "summary": "Pete Koomen of Y Combinator proposes 'Small Software' as LLMs enable extensible web apps, and Cloudflare engineer Alex Sherman argues that LLM-native software like Pi allows users to customize apps by asking, with Dynamic Workers as a fitting platform for this model.", "body_md": "Most of the web software we interact with today is static. The developers have a limited amount of time and attention, and focus on building the features that serve the largest group of users. The top of the demand curve is well-served by existing software, but there is a long-tail of unmet needs that’s different for every user.\n\n[Even if the developers were incredibly motivated to shove in every feature, user interfaces can only become so complex before they become unusable.](https://newsletter.getprimitive.ai/p/when-to-design-for-emergence) Every additional feature added complicates the product for every other user. If the market for that feature is small, it can actively make the product worse for every user who doesn’t need it.\n\nWith this context the rise of LLM-assisted coding has been genuinely empowering for anyone who needed something that fell into this long tail.\n\n## Software has gotten all… squishy[#](#software-has-gotten-all-squishy)\n\nIt’s become readily apparent that LLMs are really quite excellent at building [Software for One](https://www.ajwaxman.com/writing/software-for-one). Personal apps that side-step all of the complexity and accountability of enterprise software and are custom fit for a single person’s workflow.\n\n[Pete Koomen](https://koomen.dev/) at Y Combinator thinks there is an opportunity for what they are calling [Small Software](https://x.com/ycombinator/status/2079963726435021232). I think they are onto something.\n\n[Pi](https://pi.dev/) is a good example of what I’m starting to think of as **LLM-native software**: a battle-tested core, but almost endlessly extensible just by asking, where users are able to share their customizations with others. **In the past year your users have suddenly acquired the ability to speak code into existence.** Most existing software can’t leverage this. Pi leans into it.\n\nI suspect we’re going to start seeing more software following this self-extension pattern. However most of our existing examples of pluggable software are local software: AI agents, developer IDEs, mods for video games, Blender add-ons, CAD extensions. These tend to be professional tools with a high barrier to entry.\n\n**The web is the most successful software distribution system in the world.** It shouldn’t be left behind.\n\nMy hypothesis is that **there is a new opportunity for Extensible Software on the web**. LLMs radically lower the cost of authoring extensions, and modern sandbox primitives lower the deployment cost and provide good security boundaries. We can build our app as a solid, accountable core, and allow users to safely extend it in many directions by having LLMs fill in the missing pieces. **We can give our users super powers.**\n\nDisclosure: I currently work at Cloudflare, where high levels of exposure to [Kenton Varda](https://x.com/kentonvarda)’s writing have shaped much of my thinking here. Near the end, I’ll make the case that [Dynamic Workers](https://developers.cloudflare.com/dynamic-workers/) are a particularly good fit for this model, but I’ll cover several alternatives first.\n\n## What would this look like?[#](#what-would-this-look-like)\n\nA lot of web systems today rely on webhooks to allow the user to react to changes in the app. This ~kind of works, but it sets a really high bar for extension: building and operating a completely separate service plus dealing with whatever delivery issues arise.\n\nI want to be able to hook into record updates and slide in my own logic. “When I attach this tag to a record, run my function”. “Do this action for me on a daily cron”.\n\nActually, I don’t want to have to think about that at all. I want to tell my read-it-later app:\n\n- Please send every article I fave longer than 4000 words to my\n`<ereader of choice>`\n\n- Look for new papers published on arxiv in\n`<my specialty>`\n\neach week, add your own summary of how it relates to my work at the top, and tag it with`<tag>`\n\n- The default algorithm completely garbles\n`<site I read frequently>`\n\n. Pull a few examples and make a custom parser for it.\n\nAnd then a robot will extrude the silly bits of code, hook them into some extensions points, and make that happen. I should also be able to share what I’ve made with anyone else who might also want the same feature.[1](#user-content-fn-participation-inequality)\n\nHere are some more areas where I’d love to see an LLM-native extension approach.\n\n#### AI Agents[#](#ai-agents)\n\nOkay, this is the obvious one. [pi](https://pi.dev), [deepseek](https://www.deepseek.com/harness/en/), and [opencode](https://x.com/thdxr/status/2087945880863191162), are all experimenting in this space.\n\nRather than adding every new idea to its core, Pi provides stable hooks for tools, commands, events, and UI, so it can turn a request into a small TypeScript extension and reload it in place. Those extensions can then be bundled into packages that can be shared, letting the ecosystem absorb the long tail of ideas without bloating the harness itself.\n\nHowever the audience of these, at least as they exist now, is fairly small. You have to be comfortable running custom software on your local machine. In corporate environments **the organization** has to be comfortable with you running software that no one has ever, or will ever, look at. Unless you sandbox Pi yourself, Pi extensions run with the same permissions as Pi itself.\n\nSoftware engineers will find a way, but accountants, doctors, lawyers, and thousands of other professions deserve better tools too. They need agents that can be safely and easily tailored to their domain and their own workflows.\n\nIf we’re going to get more people using agents, that doesn’t mean making them software developers. It means making the software fit their needs.\n\n#### Internal Corporate Platform[#](#internal-corporate-platform)\n\nAll companies end up with tons of data. Employees need to view it, query it, investigate it, correlate it with this other data in this other system, find customers experiencing `<problem x>`\n\n, find customers about to churn, and a million more things.\n\nA lot of companies are experimenting with allowing AI-enthusiast employees to vibe code their own tooling, maybe deploy it to a PaaS. This is directionally correct, but creates a bunch of downstream problems. Once you have hundreds or thousands of these apps, how do you maintain them? How do they get access to the data that they need? How do they get access to **only the data that they need**? How can we audit what this software is doing? If we’re relying on access tokens, what are their scopes? Who rotates them? How do we make sure that we’re not logging out customer information to a third-party? How do we make sure we’re not violating GDPR?\n\nOr a million other compliance and security things that real businesses need to worry themselves about.\n\nWhat if we gave them a place to deploy code where there are no auth tokens that can leak? Where data access is handled by an internal platform team that can ensure all of the compliance boxes are checked? Give them the space to build their own automations or custom views, but safely.[2](#user-content-fn-vibe-coding-platforms)\n\nSpoiler: This is basically [Cloudflare OS](https://github.com/cloudflare/cloudflare-os).\n\n#### Support Platform[#](#support-platform)\n\nI’ve spent a lot of my career handling tricky support tickets. Inevitably I end up digging through dashboards, searching logs, pulling data from a million different places. Let me create extensions that surface data for the user that opened the ticket from my particular system into the support interface. Give me hooks so I can kick off agents to do the first round of investigation for me, before I even look at it. If there are common tasks that I need to do like “reset specific quota X” let me add a button to my view that can do that.\n\nThen also let me share these with my team so we can all help each other.\n\n#### Observability Platform[#](#observability-platform)\n\nA lot of Observability tooling has converged towards the same feature set: a way to search your logs with the little bar graph on top. A trace waterfall view for viewing individual traces. Customizable metrics dashboards. Maybe a service map. A few are experimenting [with new visualizations](https://embrace.io/), especially with [the rise of agents](https://www.honeycomb.io/blog/agent-timeline-flight-recorder-for-your-ai-agents).\n\nThe venerable trace waterfall diagram is very useful for systems that are shaped as request / response, where you mainly care about latency and success rate. A lot of us are finding ourselves with systems that are a bit more… stateful… or dynamic. Modern apps are running non-deterministic agents or durable workflow engines where a single action might take hours or days. Trace spans are a great source-of-truth to build upon, but let me experiment with my own visualizations (or install someone else’s).[3](#user-content-fn-frontend-sandboxing)\n\nBeyond pretty things I can look at, let me inject my own logic:\n\n- arbitrary transforms for data on ingestion\n- have alarms kick off my own scripts: deterministic code or my own agent\n- give me options to run my own code at times of highest risk: deploys or feature flag rollouts\n- if I have a special\n`MyResourceID`\n\nin my logs, let me turn that into a link that goes straight to that resource on another platform\n\n## Extensible software on the web is… harder[#](#extensible-software-on-the-web-is-harder)\n\nI just made all of that sound easy. It’s nothing of the sort.\n\nI’m a big fan of Obsidian, both as a tool I use every day and as a piece of software.\n\nIt seems like a basic markdown editor, but with a few clicks you can extend it to do just about anything: [track your tasks in a kanban board](https://publish.obsidian.md/kanban/Obsidian+Kanban+Plugin) or [turn your notes into a database](https://github.com/blacksmithgu/obsidian-dataview). Want to shove all your notes into a vector database for semantic search? [Go for it](https://motherduck.com/blog/obsidian-rag-duckdb-motherduck/)! And if you want to go further, the underlying web UI primitives are easily hackable.\n\nHowever that power comes with a cost: Obsidian’s extension model requires you to trust every plugin you install. [A plugin can basically do anything](https://cyber.netsecops.io/articles/obsidian-plugin-abused-in-campaign-to-deploy-phantom-pulse-rat/). Obsidian fights this security challenge with [automated and manual review](https://obsidian.md/blog/future-of-plugins/) and by verifying plugin authors.\n\nFor a notes app this is likely the right tradeoff. It works because the stakes are low and the community is relatively small. But this model falls apart the moment you want the same level of extensibility in software holding other people’s data: customer records, financial transactions, private messages. Extensibility and web services have always been a challenge.\n\nExecuting arbitrary code is rife with security and abuse challenges. An incomplete list:\n\n- Errors or infinite loops in the user’s code should never take down your service\n- With access to keys, customer extensions can forward them to a third party\n- Likewise if you expose sensitive data, make sure it can’t be exfiltrated\n- Make sure this system can’t be abused to do a Denial of Service attack\n- Make sure the user can’t accidentally Denial of Service you\n- Protect against\n[Spectre](https://meltdownattack.com/)attacks - If people can use free compute to mine crypto on your dime, they will\n- and many more…\n\n## But surely someone has done this?[#](#but-surely-someone-has-done-this)\n\nBefore we write this off as infeasible, there is a clear example where this kind of extensibility on the web has worked at immense scale: Salesforce.\n\nYes, that Salesforce. [And they’ve been doing it since 2007](https://www.eweek.com/enterprise-apps/salesforce-com-issues-summer-07-release-apex-code/). (As a point of reference, AWS S3 and EC2 were launched [in 2006](https://en.wikipedia.org/wiki/Timeline_of_Amazon_Web_Services).)\n\nAsk most technologists what Salesforce is and you’ll either get a blank stare or maybe something to the effect of “Aren’t they a CRM?”. However it’s more accurate to describe Salesforce as a massive multi-tenant programmable platform. In the nascent cloud era this cut against the grain: no containers, and forcing people into writing this weird, custom [Java-like language, Apex](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_intro_what_is_apex.htm).\n\nHowever with the rise of serverless, the platform starts to look a lot more familiar. Consider some examples:\n\nIf I need to expose a custom endpoint, I can do so with a few lines of code. The platform handles routing, authentication, tenant isolation, execution. There is no webserver to deploy. Squint and [you can see it as a precursor to modern serverless](https://developers.cloudflare.com/workers/examples/return-json/).\n\n```\n@RestResource(urlMapping='/customer-health')\nglobal with sharing class CustomerHealthApi {\n    @HttpGet\n    global static Account getCustomer() {\n        String accountId =\n            RestContext.request.params.get('accountId');\n\n        return [\n            SELECT Id, Name, Health_Score__c, Renewal_Date__c\n            FROM Account\n            WHERE Id = :accountId\n            WITH USER_MODE\n            LIMIT 1\n        ];\n    }\n}\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const accountId = new URL(request.url).searchParams.get(\"accountId\");\n\n    const account = await env.ACCOUNTS.prepare(`\n      SELECT id, name, health_score, renewal_date\n      FROM accounts\n      WHERE id = ?\n      LIMIT 1\n    `)\n      .bind(accountId)\n      .first();\n\n    return Response.json(account);\n  },\n} satisfies ExportedHandler<Env>;\n```\n\nOr what about running custom logic [on a schedule](https://developers.cloudflare.com/workers/examples/cron-trigger/)?:\n\n```\npublic class RenewalScanner implements Schedulable {\n    public void execute(SchedulableContext context) {\n        List<Account> accounts = [\n            SELECT Id, Needs_Attention__c\n            FROM Account\n            WHERE Renewal_Date__c = NEXT_N_DAYS:30\n            WITH USER_MODE\n        ];\n\n        for (Account account : accounts) {\n            account.Needs_Attention__c = true;\n        }\n\n        update as user accounts;\n    }\n}\n\n// Schedule it to run daily at 2 a.m.:\nSystem.schedule(\n  'Check upcoming renewals',\n  '0 0 2 * * ?',\n  new RenewalScanner()\n);\nexport default {\n  async scheduled(controller: ScheduledController, env: Env) {\n    await env.ACCOUNTS.prepare(`\n      UPDATE accounts\n      SET needs_attention = TRUE\n      WHERE renewal_date BETWEEN date('now')\n        AND date('now', '+30 days')\n    `).run();\n  },\n} satisfies ExportedHandler<Env>;\n{\n  \"name\": \"renewal-scanner\",\n  /* ... */\n  \"triggers\": {\n    \"crons\": [\"0 2 * * *\"]\n  },\n}\n```\n\nThere are also higher-level primitives so you can point-and-click your way into a custom application, but at its heart Salesforce is safely running your custom logic directly in response to app events, within transactions, and allowing you to encode the particulars of your business into their app.\n\nTwo decades ago Salesforce didn’t have a ton of options for a way to cheaply run sandboxed code on behalf of their users, so they built out a compiler, type system, runtime, standard library, debugger, integrated SQL into the language, lots of fancy database tricks and heaps more, and then built out a whole educational ecosystem. The problems it solved for businesses were valuable enough to justify hiring humans who specialized in their particular development platform.\n\nWe can be inspired by what they’ve done without copying it exactly. We have a lot more options in 2026, so let’s look at what the technical requirements are for building something like this, and then what technologies might fit.\n\n## New writing, occasionally.\n\nGet my posts in your inbox. No fixed schedule, no noise.\n\n## A new primitive[#](#a-new-primitive)\n\nWe need a primitive to build this extensibility around. In order to make it work, it needs to have a couple of properties.\n\n~~Cheap~~ Economical to run[#](#cheap-economical-to-run)\n\nIf you are going to have thousands or millions of users running snippets of custom code, the idea of spinning up a custom-container-per-user is a non-starter. It needs to cost ~$0 when it’s not being executed, and each execution ideally needs to be tiny-fractions-of-a-penny cheap.\n\nAdd to that cost to build or compile, store the built artifacts, collect logs, and more. Especially with RAM prices in 2026, how much memory overhead is required to serve a request will largely determine how many users you can pack onto a single machine.\n\n#### Fast cold starts[#](#fast-cold-starts)\n\nWe all want our web services to be fast, so if we’re running user code as part of the critical path of responding to a request, we can’t wait a minute plus for a container to spin up. Ideally a cold start is measured in single-digit milliseconds.\n\nIf you are only offering extensions that respond to events or run on a schedule you can likely afford higher startup times.\n\n#### Control over limits[#](#control-over-limits)\n\nUsers of platforms do all sorts of weird, edge-case things. One of my favorite stories from an engineer at Heroku was that someone had published a very popular getting-started guide that had the user deploy the following Python app:\n\n```\nwhile True:\n   print(\"hello world!\");\n```\n\nFrom the system’s perspective you have a brand-new app suddenly come into existence and immediately start\nspewing millions of lines of logs per second that will never stop, and the user expects something\nreasonable to happen when they run the `tail`\n\ncommand.\n\nTo protect your system you need to be able to enforce limits on basically everything: CPU, memory, number and size of network requests, response size, log volume and rate, and much more.\n\n#### Solid isolation boundary[#](#solid-isolation-boundary)\n\nI mean this in both the fault isolation and security isolation senses. No matter what the user does: crashes, runs an infinite loop, allocates memory as fast as possible, it should have no effect on any other user.\n\nAnd actively malicious code must not be able to escape or inspect other tenants. This includes speculative execution attacks like [Spectre](https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)).\n\n#### Allow the code to take actions (safely)[#](#allow-the-code-to-take-actions-safely)\n\nCustom code that can’t affect anything is useless, so we need some controlled way for user code to interact with the rest of the world. In the simplest case you can model things as a pure function. The user’s code receives some data as input and can respond with an answer. If there is no I/O allowed, and a constrained output, this is quite safe, if limiting.\n\n```\nexport default function shouldWeOrderPizzaTonight(data: Input): boolean {\n  // consider the options very carefully\n  const haveFoodAtHome = data.fridge.hasIngredients;\n  const haveEnergy = data.body.checkCapacity;\n  const haveTime = !data.schedule.isTight;\n  \n  // return haveFoodAtHome && haveEnergy && haveTime;\n  // we don't believe in data-driven decision making in this household\n  return true;\n}\n```\n\nIf you need to expose more to the user, then things get a little more tricky. When we want our own code to call an API, we typically add some sort of API key that we can attach to our requests:\n\n``` js\nconst response = await fetch(api, {\n  headers: {\n    Authorization: `Bearer ${env.API_KEY}`,\n  },\n});\n```\n\nBut this kind of flexibility is dangerous! Malicious code can immediately leak that data by `POST`\n\ning it to a\nthird-party. Even exposing raw `fetch`\n\nmeans that the user can now use your infrastructure to DoS someone if they want.\n\nThe most common solution for this today is adopting a proxy. The user is given an opaque token that is meaningful only\nto the proxy. The proxy validates the request, and then replaces the opaque token with the real credential, before\nforwarding the request to the destination. The proxy can also\nenforce an allowlist of possible destinations and rate-limits on requests. This is strictly better than raw `fetch`\n\n,\nbut still has some problems.\n\n``` js\nconst response = await fetch(apiViaProxy, {\n  headers: {\n    Authorization: `Bearer REPLACE_THIS_WITH_MY_API_KEY_IN_PROXY`,\n  },\n});\n```\n\nYou may want to restrict what the code can do to only a subset of what the API allows, which requires very\nfine-grained authentication that most APIs do not offer. [There can be pretty dire consequences if that API\nprovides too much power, or is exploitable in ways you cannot foresee.](https://www.youtube.com/watch?v=87DyyMV0kCY)\n\nEven if the service provides fine-grained permissions, like the ability to read your email, that may still be far more access than you want to give the code. If you want to give the code only access to one specific email, there’s generally no token you can generate that allows only this.\n\nYou can try to enforce that in a proxy, but now you are tasked with filtering out all requests that don’t match some narrow set of criteria, and keeping that up-to-date as the backing API evolves. Our proxy code quickly becomes very complicated. It’s difficult to anticipate everything a user might do here. Testing this logic and making sure it’s bulletproof is challenging.\n\n``` js\nasync function proxyFetch(url: URL, headers: Headers) {\n  const opaqueToken = headers\n    .get(\"Authorization\")\n    ?.replace(/^Bearer\\s+/i, \"\");\n\n  const grant = await parseToken(opaqueToken);\n\n  if (!grant || grant.action !== \"read-email\") {\n    throw new Error(\"Forbidden\");\n  }\n\n  const allowedPath = \n    `/email/v1/users/messages/${encodeURIComponent(grant.messageId)}`;\n\n  if (\n    url.origin !== \"https://email.service.com\" ||\n    url.pathname !== allowedPath\n  ) {\n    throw new Error(\"Forbidden\");\n  }\n\n  const newHeaders = new Headers();\n\n  // Forward only explicitly permitted headers.\n  for (const name of [\"accept\", \"if-none-match\"]) {\n    const value = headers.get(name);\n\n    if (value !== null) {\n      newHeaders.set(name, value);\n    }\n  }\n\n  // Replace the opaque token with the real credential.\n  newHeaders.set(\"Authorization\", `Bearer ${EMAIL_API_KEY}`);\n\n  return fetch(url, { headers: newHeaders });\n}\n```\n\nAnd this is the filtering logic for just one operation on just one endpoint. In general, starting with a lot of power and then trying to restrict it precisely is a hard problem.\n\nA better way is to hand the untrusted code a [narrow capability](https://en.wikipedia.org/wiki/Object-capability_model).\nAt a high level you can think of a capability as a reference to a specific function, such as one for fetching one approved-in-advance email:\n\n``` js\n// Trusted host code\nconst getApprovedEmail = () => fetchEmailById(123, auth);\n\n// Untrusted extension code\nexport default async function doSomethingWithAnEmail(\n  { getApprovedEmail }: Capabilities,\n) {\n  const email = await getApprovedEmail();\n  // do something with the email\n}\n```\n\nIf we remove ambient I/O, **the code can only take actions via the references it has been passed**.\nThis pattern is **much** easier to reason about. We don’t have to muck around with complicated\nproxy logic. The API credential is never exposed to the untrusted code at all. And without some other\noutbound capability, there’s no way to leak data.[4](#user-content-fn-cloudflare-bindings)\n\nAs a bonus, generating logic from a TypeScript definition of capabilities is much easier and token-efficient for an LLM than handing it a pile of OpenAPI JSON definitions.\n\nIf you are familiar with [IFTTT](https://ifttt.com/), it doesn’t give you a Twitter API key, it gives you\n[ twitter.post_new_tweet()](https://ifttt.com/twitter/actions/post_new_tweet). You don’t get a full email\nclient, you get\n\n[.](https://ifttt.com/email/actions/send_me_email)\n\n`email.send_me_email`\n\nThis is the shape we generally want for safe extensible software.\n\n## What technology fits?[#](#what-technology-fits)\n\nThe more agent-brained among you have noticed by now that these are the same properties that you are looking for from an agent execution platform. That’s not a coincidence! This is essentially the same problem: how can you run logic on behalf of a user that you cannot trust.\n\nThe solution space has a number of options:\n\n#### Interpreter[#](#interpreter)\n\nBuilding their own language worked for Salesforce twenty years ago, and [this pattern still\nworks today](https://docs.langchain.com/oss/python/deepagents/interpreters).\n\nYou can use an off-the-shelf embeddable interpreter like [Lua](https://www.lua.org/) or\n[QuickJS](https://bellard.org/quickjs/) or [roll your own](https://x.com/huntlovell/status/2071985800909410797).\n\n#### V8 Isolates[#](#v8-isolates)\n\nIf you take the interpreter approach to it’s logical conclusion, you’ll eventually end up wanting to move to bytecode, and adding a JIT, and…\n\nJumping straight to V8 saves you the time. Google has dumped enormous amounts of money and developer time into hardening the V8 JavaScript\nengine. Cloudflare uses [v8 isolates](https://developers.cloudflare.com/workers/reference/how-workers-works/)\nas its isolation boundary for Workers, but it’s not the only option in this space.\n\n- Node has\n`isolated-vm`\n\n- Rivet has\n`secure-exec`\n\n#### MicroVMs[#](#microvms)\n\nFull VMs emulate a lot of virtual hardware: USB, graphics, disks, etc, which is what allows you to run full desktop environments in them, but that comes at a cost. Millions of lines of code and complexity that needs to boot up and takes up resources.\n\nMicroVMs strip that back to the bone, running very constrained operating systems, but the payoff is that they can start in under a second and have a very small memory overhead with strong isolation boundary.\n\nMicroVMs have more overhead than the other options, but have some distinct benefits:\n\n- POSIX\n- potential to utilize a lot of CPU and RAM\n- full OS capable of running binaries\n\nIf you mainly want to allow the user to run some bit of logic, call some API endpoints, run a workflow, then the overhead of this approach might make it overkill. However even if you go with something like V8 isolates or WASM as your isolation primitive, microVMs could still be quite useful for authoring, compiling / bundling, and testing user extensions.\n\nThis is a **very** hot space with a lot of options:\n\n[Firecracker](https://github.com/firecracker-microvm/firecracker)`libkrun`\n\n[AWS Lambda MicroVMs](https://aws.amazon.com/lambda/lambda-microvms/).`@deno/sandbox`\n\n[smolvm](https://smolmachines.com/)[Tensorlake](https://www.tensorlake.ai/)[Daytona](https://www.daytona.io/)- Probably a million more…\n\n#### WASM + WASI[#](#wasm--wasi)\n\nWebAssembly starts out with a blank slate. The code can run, allocate memory, but there are no built-in modules for making an HTTP request, or reading an environment variable. This makes it an attractive candidate from a security perspective!\n\n[WASI](https://wasi.dev/) defines a standard interface where the host can define the capabilities that\nget passed to the untrusted WASM code.\n\nBy integrating at this lower level, you can get a lot of potential performance and allow users to write in any language that can compile to WebAssembly, but the tool chain grows significantly in complexity.\n\nYou can also run WebAssembly within a V8 isolate or [microVM](https://opensource.microsoft.com/blog/2025/03/26/hyperlight-wasm-fast-secure-and-os-free/). None of these options are mutually exclusive.\n\nIf the isolation primitive does not provide its own capability model, it’s still a useful way of thinking\nthrough how you expose functionality. A proxy can work in some cases, but you should also consider using\nan Object Capability protocol like [Cap’n Web](https://github.com/cloudflare/capnweb) with any of these\nprimitives.\n\nHowever there’s one solution here that I want to highlight in particular…\n\n## Cloudflare’s Dynamic Workers[#](#cloudflares-dynamic-workers)\n\nCloudflare’s [Dynamic Workers](https://developers.cloudflare.com/dynamic-workers/) were built with exactly this kind\nof use in mind. The marketing for them has (understandably) been focused on [code mode](https://blog.cloudflare.com/code-mode/) and agent use-cases, but IMO it’s much broader than that.\n\nBeyond meeting the criteria I proposed above, they are the closest thing to a production-ready out-of-the-box framework for building extensible web apps that I’ve been able to find in 2026. (But I bet there will be more soon)\n\nThere are a handful of things that they provide that you’ll need to build out yourself with other solutions:\n\n#### Observability[#](#observability)\n\n(My day job and personal soapbox)\n\nBoth you and your users need visibility into what their code is doing. Cloudflare Workers [have OpenTelemetry\ntracing built into the runtime itself](https://blog.cloudflare.com/workers-tracing-now-in-open-beta/) and have\n[first-class primitives](https://developers.cloudflare.com/workers/observability/logs/tail-workers/) that allow\nyou a lot of control over emitted telemetry.\n\n#### Multi-tenant data storage[#](#multi-tenant-data-storage)\n\nWhile not every extension system needs users to be able to store their own data, this gives users a lot more flexibility.\n\nGive them their very own SQLite database with [Durable Object facets](https://blog.cloudflare.com/durable-object-facets-dynamic-workers/). [Or give them their own R2 bucket](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/).\n\n#### Durable Execution[#](#durable-execution)\n\nThe rise of [Temporal](https://temporal.io/) et al has shown that a lot of problems benefit from Durable Execution. [Dynamic Workflows](https://blog.cloudflare.com/dynamic-workflows/) lets users to take actions over minutes or days, with appropriate retries and backoff.\n\n#### Source Control[#](#source-control)\n\nUsers probably need to version and iterate on their extensions, and you can’t expect that everyone uses GitHub. [Build source control into your product](https://www.cloudflare.com/products/artifacts/).\n\n#### Hosted LLMs[#](#hosted-llms)\n\nUsers can use LLMs to help draft their extensions, but you can also expose LLMs through [Workers AI](https://www.cloudflare.com/products/workers-ai/) so users can use them in their extensions (with appropriate token budgets and rate limits).\n\n```\nexport async function analyzeArticle(env: Env, article: Article) {\n  return result = await env.AI.run(\n    messages: [\n      {\n        role: \"system\",\n        content: \"Decide whether the supplied article talks about cute kittens.\",\n      },\n      {\n        role: \"user\",\n        content: article.text,\n      },\n    ],\n  )\n}\n```\n\n#### Self-hosting JavaScript Tooling[#](#self-hosting-javascript-tooling)\n\nA lot of JavaScript tooling is itself written in JavaScript, which means that building and testing extension code might not need a separate container or VM.\n\n``` js\nimport { transform } from 'sucrase';\n\nexport function transpileUserCode(source: string): TranspileResult {\n  try {\n    const result = transform(source, {\n      transforms: ['typescript'],\n      disableESTransforms: true\n    });\n    return { type: 'success', code: result.code };\n  } catch (err) {\n    return { type: 'failure', error: String(err) } };\n  }\n}\n```\n\n## Demo Time[#](#demo-time)\n\nAs I was writing this post I thought “What if I turned my static blog into the world’s\nsmallest vibe-coding platform?”[5](#user-content-fn-byo-vibes)\n\nI wanted to include a guide to working with Dynamic Workers and some cool demos, but\nthis blog post is already way too long. I split that out into a guide to [Working\nwith Dynamic Workers](/blog/working-with-dynamic-workers) but still wanted to embed\nthe final demos here.\n\nThe demo’s harness is based around the idea of a customizable scraper. Given a URL, it will fetch the contents (unless they block Cloudflare), and pass those contents and a few utilities to the user’s code. See the guide for a full explanation.\n\nAll of the examples run on Cloudflare Workers, and the source is editable. Modify any of them to run your own script, or if you want to write your own choose “Write your own” and there’s an LLM prompt to get you started.\n\nEach example runs through the same harness, but exercises a different combination\nof libraries and capabilities. Choose one, pick a suggested URL, or your own, and hit **Run**.\n\n## Here be dragons[#](#here-be-dragons)\n\nOne last thought.\n\nI’ve worked at platforms for almost a decade. I don’t mean to make “turn your app into a platform” sound easy. Platforms are hard: hard to design, hard to run, hard to debug.\n\nExposing APIs to customers means a lot of upfront thought, and long-term support (though maybe LLMs can make this a lot easier?).\n\nBut they are also really fun, both as a user and a creator. You can be truly surprised by the creativity of your users as they do things that you never considered or would have even thought possible.\n\nPlatforms are hard, but it’s worth it.\n\n## Appendix[#](#appendix)\n\nSome things that were influential in drafting this blog post:\n\n[Kenton Varda](https://x.com/KentonVarda)’s many writings and talks[Cloudflare OS](https://os.cloudflare.app/)[Ink and Switch](https://www.inkandswitch.com/)’s[Malleable Software](https://www.inkandswitch.com/essay/malleable-software/)[Andy Matuschak](https://andymatuschak.org/)’s[Apps and programming: two accidental tyrannies](https://andymatuschak.org/tat/)\n\n## Footnotes[#](#footnote-label)\n\n[1.](#user-content-fnref-participation-inequality)I suspect that even in a fully LLM-accelerated world\n\n[participation equality](https://www.nngroup.com/articles/participation-inequality/)is still going to be A Thing. A small percentage will author most of the extensions in any given ecosystem, no matter how easy we make it.[↩](#user-content-fnref-participation-inequality)[2.](#user-content-fnref-vibe-coding-platforms)If you squint, vibe coding platforms are kind of a generic version of this, except instead of providing custom functionality for your organization, they provide generic data storage and hosting. I expect they will start to add this kind of customized hosted access as they start selling to Enterprise.\n\n[↩](#user-content-fnref-vibe-coding-platforms)[3.](#user-content-fnref-frontend-sandboxing)This completely glosses over a need to sandbox UI on the client side where custom code can access potentially sensitive data.\n\n[That topic deserves its own post.](https://github.com/endojs/endo/tree/master/packages/ses). Or point your robot atand ask it how it’s done there.`cloudflare-os`\n\n[↩](#user-content-fnref-frontend-sandboxing)[4.](#user-content-fnref-cloudflare-bindings)If you’re familiar with Workers, you might be thinking “this looks a lot like bindings…”. Yes! Bindings and Service Workers work on an\n\n[Object Capability RPC system](https://developers.cloudflare.com/workers/runtime-apis/rpc/). You can think of exposing capabilities to users as generating bindings for your particular service.[↩](#user-content-fnref-cloudflare-bindings)[5.](#user-content-fnref-byo-vibes)You’ll have to bring your own vibes though. I decided “expose free LLM usage to the internet” was probably not in my best financial interest.\n\n[↩](#user-content-fnref-byo-vibes)", "url": "https://wpnews.pro/news/extensible-software-in-the-age-of-llms", "canonical_source": "https://jeremymorrell.dev/blog/extensible-software-in-the-age-of-llms/", "published_at": "2026-08-18 13:25:03+00:00", "updated_at": "2026-08-18 13:41:35.762418+00:00", "lang": "en", "topics": ["large-language-models", "generative-ai", "ai-products", "developer-tools"], "entities": ["Y Combinator", "Pete Koomen", "Pi", "Cloudflare", "Kenton Varda", "Dynamic Workers"], "alternates": {"html": "https://wpnews.pro/news/extensible-software-in-the-age-of-llms", "markdown": "https://wpnews.pro/news/extensible-software-in-the-age-of-llms.md", "text": "https://wpnews.pro/news/extensible-software-in-the-age-of-llms.txt", "jsonld": "https://wpnews.pro/news/extensible-software-in-the-age-of-llms.jsonld"}}