Introducing Base
A framework for Cloudflare Workers, built for the software you intend to keep.
Today I am very proud to introduce the Base framework to the world. A backend framework for building serious applications on Cloudflare Workers. We didn't get here because I wanted to make a framework; it exists because it was necessary to do what we wanted.
When we started Phi.health we knew we wanted something different than what we had worked with previously. We set out with the following goals in mind:
- Operational simplicity: Move the ops burden to the cloud provider as much as possible. We didn't want to host anything ourselves.
- Affordability: We are a startup, so we want to be able to run our infrastructure as cheaply as possible.
- Easy to develop: We wanted to enjoy the day to day work, rather than feel like we were fighting the language or the tooling.
After doing the research Cloudflare Workers was the obvious choice, and it had some real wins:
- Autoscaling: With Cloudflare you don't pay to have the capacity you might not need. It scales to fit the demand, and you only ever pay for what you use.
- Edge: Your code lives on Cloudflare's Edge servers and runs closer to the end user.
- Wrangler: Convenient local development using JavaScript or TypeScript (at the time): all you have to do is implement
fetch()and you are up and going.
But soon after the questions began to emerge:
- How would we manage data located in a database (there was no D1 yet)?
- How can we add GraphQL to our API?
- How should we return errors?
- How should we validate input so that every surface agrees?
- How should my tests be organized?
Cloudflare Workers with its elegant simplicity was also completely silent on a large amount of decisions that every project of any consequence will have to answer eventually.
And as Phi grew, so did the questions:
- Once we had more than one worker, how should they call each other?
- How do we keep configuration and secrets straight across environments?
- How do we share code between workers without copy-pasting it?
Every team on Workers answers those questions. Most answer them twice, because the first answers were made in a hurry and never written down.
Coming from the mobile app development space I was used to a more prescribed solution. All mobile application platforms ship entire IDEs dedicated to building apps for their platforms. Providing an editor, tooling, and a class library to get the job done. I took inspiration from this experience and Base began to take shape.
Base is the missing layer: a framework for Cloudflare Workers, built for software you intend to keep.
What Base is
Base is a command line interface, a class library, and VSCode settings and lint rules. Together they give a
Cloudflare Workers project a more grounded starting point than an empty fetch handler, and organizational
structure that holds up as it grows.
-
base-cli: This is the command line interface. It's used for all aspects of your project, from developing to deploying your workers to generating and running database migrations. It manages not one worker but a fleet of them, their configurations, their databases, and their deploys. It plays a central role in making the workers experience smooth and error free. -
base-foundation: The framework itself, and the package your worker is built against. It provides the dependency injection container, the module system, and every dispatch surface (HTTP routing, GraphQL, RPC, queues, scheduled tasks, and WebSockets), along with the ORM, validation, serialization, access control, and the error model they all share. -
base-common: Environment-agnostic helpers thatbase-foundationis built on, and that your own code can use directly: concurrency primitives, encryption services,Decimal-backed currency, time and ISO-8601 duration handling, the shared error model, and the RPC wire protocol. -
base-lint: Lint rules used by the CLI and VSCode to ensure your code follows the rules and conventions of the framework. -
base-client: An optional client that can be used by external projects to call Base applications via RPC.
What a worker looks like
A Base worker is three files, and the interesting one is settings.ts, a single
description of what the worker is, read by both the CLI and the runtime.
services is one list, not five. There is no separate registry of routes,
resolvers, queue processors, or scheduled jobs. Each class says what it is, on
itself, and Base sorts them at boot:
CreateNoteInput is validated before your method runs, by the same
pipeline that validates GraphQL arguments and RPC parameters. A class listed in services with
no recognized decorator is not ignored, it is a boot error. Registration is
explicit, and ambiguity is treated as a bug rather than a judgment call.
That shape holds across every surface. HTTP,
GraphQL, RPC,
queues,
scheduled work, and
WebSockets are all decorated classes in the
same services list, guarded by the same validation, speaking the same error taxonomy.
Underneath them sits a typed ORM with real
migrations,
dependency injection, and a
module system that moves code between
workers with one line of registration.
Design goals
Base was built with me as its primary consumer, which is a narrow audience but an unforgiving one. I wanted something I would still enjoy using on the fourth project, not just the first. These are the principles I tried to adhere to:
- Loosely coupled, easily testable code
- Integration tests as a first party citizen
- Equally at home in the Startup or the Enterprise
- Leverage Open Source to solve truly hard problems
- Make it hard to do the wrong thing
- Optimize the code for long term maintainability
- Reduce opportunities for misuse and side effects
- One source of truth for every fact
- Fail at boot rather than in production
- Safe defaults for the developer who forgets
One of those deserves more than a bullet, because it explains most of the rest.
Most frameworks optimize the first five minutes: the terse demo, the one-line route, the magic that fits on a conference slide. Base optimizes a different moment, the read six months later, by someone with no context.
That single choice explains most of the design. Imports name their exact source. Classes state what they are, on the class. Nothing important is more than one hop from where you are standing. A Base application usually runs a few lines longer than the hand-rolled equivalent, and nearly all of those lines are declarations, the cheapest lines in any codebase and the only ones your compiler, linter, and docs tooling can actually read.
It is a real tradeoff, made deliberately, in one direction. If you want the shortest possible first file, Base will lose that comparison. It is not trying to win it.
I didn't start from scratch
My position building Base was to lean on open source for the genuinely hard problems and write our own code only where it was more prudent. A lot of excellent work sits underneath the framework.
- Drizzle - Generates the SQL beneath our ORM and drives migrations. Base layers entities, repositories, and the migration workflow on top of it.
- TypeGraphQL - Turns decorated classes into a GraphQL schema. We maintain a fork so schema building integrates with Base's container, validation, and error handling.
- GraphQL Yoga - Serves that schema over HTTP, handling the parts of the GraphQL specification we have no business reimplementing.
- IttyRouter - Matches incoming requests to routes. Small, fast, and designed for workers, which is why Base's router sits on it instead of one of our own.
- Tsyringe - Provides the dependency injection container. We extend it with Base's scope hierarchy, worker-scoped lifetimes, and typed injection tokens.
- reflect-metadata - Makes decorator metadata readable at runtime, which is what lets a class declare what it is.
- Jest - Runs both the unit tests beside your code and the integration suite against a live worker.
- Prettier - Formats everything, import sorting included, wired into the pre-commit hook every scaffolded workspace ships with.
- ESLint - Runs the rules
base-lintships, so the framework's own contracts are enforced in your editor and in CI.
A longer list lives in the repository, including the pieces that quietly do a lot
of work: smol-toml for reading wrangler.toml and env.toml,
yargs behind the CLI, libsodium-wrappers for encrypting the secrets export,
and the database drivers better-sqlite3 and @planetscale/database.
Special thanks
The earliest version of Base was built on TypeORM and class-validator. Both shaped how Base thinks about entities and validation, and the debt is still visible in the code today.
Start here
Installing takes one command, and needs no Cloudflare account until you deploy:
From there, Installation gets a worker answering requests on your machine, and Your First Worker builds a typed, validated JSON API out of it. When you want a database, Add a Database is the next stop; when you want it live, Deploy to Cloudflare ships it to the edge.
We have spent over three years building Base because we needed it to exist. It is now open source under Apache 2.0, and if you are building something on Workers, I hope it saves you some of the road we walked.
Base is not perfect. If you find an issue, or want to talk something through, please reach out via GitHub or mail. I look forward to seeing what you build!