Introducing Base

The missing framework for Cloudflare Workers

Cloudflare Workers got serious. Compute, SQL, object storage, queues, cron, WebSockets and more, all globally distributed with almost nothing to operate. The conventions never arrived with it. Base is the missing piece: build a Worker as a set of declarative modules, wired by a dependency‑injection container, with one consistent path from every event to a response.

$npx @system-inc/base-cli workspace create my-app

No Cloudflare account required for local development.

Decorate a class. Add it to settings. That is the application.

Every Base application is the same two moves, whether it serves one route or a fleet of workers.

Decorate the class

A class states what it is, on the class. The route, what it accepts, and what it returns all sit with the code that runs them. The body arrives deserialized and validated, so invalid input never reaches your code.

source/services/NoteService.ts
@Injectable()
@HttpService()
export class NoteService {
    constructor(
        @Inject(NoteStore)
        private readonly noteStore: NoteStore,
    ) {}

    @HttpRoute('POST', '/notes')
    create(
        @HttpBody(() => CreateNoteInput) input: CreateNoteInput,
    ): Note {
        return this.noteStore.create(input);
    }

    @HttpRoute('GET', '/notes/:id')
    get(@HttpPath('id', () => Number) id: number): Note {
        const note = this.noteStore.find(id);
        if (!note) {
            throw HttpErrors.notFound();
        }
        return note;
    }
}

Add it to settings

A worker and its capabilities are defined in settings.ts. Its name and version, the server it binds, the modules it composes, and every class it runs are all described in one place, so there is one file to read to know what a worker is.

settings.ts
export const Settings: BaseSettings = {
    name: 'app',
    version: '1.0.0',
    title: 'app',
    server: {
        '@default': { port: 3000, host: 'localhost' },
    },
    modules: [AccountModule, BillingModule],
    // One flat list for every kind of class. No separate list of
    // routes, resolvers, or processors. Each class's decorator
    // declares the surface it dispatches on.
    services: [NoteService, NoteStore],
};

The decorator is the declaration

A class states what it is, on the class. @HttpService means it serves HTTP routes, @GqlResolver means it serves GraphQL operations, @OrmTable means it is a database table. Base sorts each class into its dispatch surface at boot.

Registration is explicit, never magic

No filesystem scanning, no convention matching, no reflection-driven discovery. A class exists in your application because you added it to settings, and a class with no recognized decorator is a boot error, not a silent no-op.

Six surfaces. One shape.

An HTTP request, a GraphQL operation, a worker-to-worker call, a queue message, a cron run, a socket connection. Base handles all six the same way: a decorated class in the services list, resolved on its own scoped container, speaking the same error taxonomy. Learn the pattern once and it holds everywhere.

HTTP

@HttpService

Routes bound to methods, with path segments, query parameters, headers, and JSON bodies deserialized and validated before your handler runs.

GraphQL

@GqlResolver

Code-first types and resolvers. The schema is generated from your classes, and a lint rule holds your GraphQL nullability to your TypeScript types.

RPC

@RpcService

Typed worker-to-worker calls over a framework-free interface either side can hold in one file. Visibility defaults to internal, so nothing leaks by omission.

Queues

@WorkerQueueProcessor

Message processors resolve from their own scoped container, with the attempts, id, and timestamp of the message on the context. Retries are deliberate: ask for one with the backoff you want.

Scheduled

@ScheduledExecutable

Cron-triggered work declared next to the code it runs, instead of in a configuration file nobody reads.

WebSockets

WebSocketDelegate

A delegate per endpoint that authorizes upgrades and reacts to socket lifecycle. On Cloudflare, each connection is backed by a Durable Object and survives hibernation.

The substrate every serious application needs

Base was not designed on a whiteboard. It was built one feature at a time for real product needs and experiences specific to Cloudflare Workers.

Declarative modules

Compose an application from modules that carry their own services, resolvers, processors, and entities. Modules are portable across workers and can attach to a different database per worker with one registration line.

Dependency injection with real scopes

A scope hierarchy from global to worker down to request, queue, scheduled, and websocket. Every event runs in a fresh child container that resolves your handler and disposes when the work is done.

An ORM with migrations

Decorator-defined entities backed by Drizzle, with CLI-driven migrations. Every column states its database type explicitly, so nothing about your schema is inferred from a TypeScript type.

A CLI that is part of the framework

Scaffold, develop, bundle, test, tail, and deploy from one tool. Validate your settings against your wrangler bindings, your ORM, and your GraphQL baseline before anything ships.

Cloudflare or Node

The framework is written to be portable and is equally at home in a pure Node environment as it is running on Cloudflare.

Conventions with teeth

Base ships its own lint rules that verify the framework contracts at compile time. The architecture survives contributors who have not read the docs, because it never depended on them being read.

Most frameworks optimize the first five minutes. Base optimizes the read, six months later, by someone with no context.

That choice shapes everything. Base values clarity of expression above all else. Each line of code communicates its intent, and every behavior is explicit.

Explicit, never hidden

No filesystem scanning, no convention matching, no reflection-driven discovery. Every behavior traces back to a declaration you can see, and imports name their exact source.

Ambiguity is an error

Where a framework could guess, Base refuses. A duplicate key throws. A column type is stated, never inferred. The moment a wrong answer is cheapest to fix is boot, so Base spends real machinery moving failures there.

Safe when someone forgets

Every default answers one question: what happens if the developer never touches this? Errors are sanitized before they reach a client. RPC visibility defaults to internal. Introspection is off outside development. Secrets do not stringify.

Loosely coupled, by construction

The framework calls your code; your code never has to call the framework. Services are plain classes with injected dependencies that are constructible and testable without booting anything. Strip Base away and your business logic still stands.

No prescribed architecture

Base has opinions about code, not about your system. One monolithic worker, a fleet of small services, a shared database or one per worker, each of these are Base applications, and you can change your mind later.

The toolchain is part of the framework

The first commit shipped a CLI and a scaffolding system before it shipped application features. Building, checking, testing, and deploying are framework concerns, not an exercise left to your CI configuration.

Base is the layer underneath what you build

The part every application needs and none should have to invent. So that what you build on it is only, entirely, yours.

$npx @system-inc/base-cli workspace create my-app

Apache 2.0 licensed. Requires Node.js 22 or later.