Count the versions of your last real-world app. There is the Express or Nest version for the server. The handler you extracted and mangled for Lambda. The API routes you re-declared when the frontend moved to Next. And now someone is asking for an MCP server so agents can use it.
Same domain logic. Four incarnations. Every framework you picked made you pay this tax, because every framework has a home runtime and your code belongs to it.
Stone.js starts from the opposite premise: an application is not an object, it is an act. Application = Domain x Context
. You write the domain once; the context (server, function, browser, agent) applies itself to your domain at runtime, not the other way around.
A domain is just classes or functions. No base class, no framework import in your business logic:
import { Service } from '@stone-js/core'
import { IncomingHttpEvent } from '@stone-js/http-core'
import { Get, Post, EventHandler } from '@stone-js/router'
@Service({ alias: 'tasks' })
export class TaskService {
private readonly items: Task[] = []
list (): Task[] { return this.items }
add (title: string): Task { /* ... */ }
}
@EventHandler('/tasks')
export class TaskController {
private readonly tasks: TaskService
constructor ({ tasks }: { tasks: TaskService }) { this.tasks = tasks }
@Get('/')
list (event: IncomingHttpEvent): Task[] {
return this.tasks.list()
}
}
Where does it run? That is not the domain's problem. It is declared once, at the edge of the app:
@NodeHttp() // a Node HTTP server
@AwsLambdaHttp() // an API Gateway handler
@Routing()
@StoneApp({ name: 'tasks' })
export class Application {}
Each decorator is an adapter: it captures the platform's raw event (an IncomingMessage
, a Lambda payload), normalizes it into one IncomingEvent
, and turns your response back into whatever the platform expects. Your controller never learns which one ran. stone run
collapses the superposition into the target you deploy.
Adapters today: Node HTTP, AWS Lambda, Azure Functions, GCP Cloud Functions, Tencent SCF, Alibaba FC, edge/fetch runtimes, browser, CLI, WebSockets. One domain, any of them.
Every single feature exists in two first-class forms, decorators or plain functions. Not a fallback, a parity contract:
import { defineEventHandler, defineRoutes } from '@stone-js/router'
const TaskController = ({ tasks }) => ({
list: (event) => tasks.list()
})
export const routes = defineRoutes([
[defineEventHandler(TaskController, 'list'), { path: '/tasks', method: 'GET' }]
])
The decorators are TC39 2023-11 stage 3 (Symbol.metadata
), not experimentalDecorators
, and there is no reflect-metadata anywhere. ESM only.
Your app can expose itself to AI agents. In development, stone mcp
starts an MCP server that lets your coding agent introspect the app it is working on (routes, services, blueprint, framework knowledge). And the same adapter mechanism that serves HTTP can serve your domain to agents as tools. The fourth rewrite is the one you skip.
Numbers you can verify, not adjectives:
@stone-js/*
(MIT), a micro-kernel that depends on exactly 3 of themThe API is settled enough that I document both paradigms for everything, and unstable enough that your feedback can still bend it. That is exactly the window where trying a framework is the most fun.
I am looking for a dozen pilots: developers, architects, QA. Build something small, break it, tell me everything that hurts. You get direct access to me and founding-tester credit.
Your app exists in every runtime. Until you run it.
Happy Entanglement. 😎